fix(network-proxy): add unix socket allow-all and update seatbelt rules (#11368)

## Summary
Adds support for a Unix socket escape hatch so we can bypass socket
allowlisting when explicitly enabled.

## Description
* added a new flag, `network.dangerously_allow_all_unix_sockets` as an
explicit escape hatch
* In codex-network-proxy, enabling that flag now allows any absolute
Unix socket path from x-unix-socket instead of requiring each path to be
explicitly allowlisted. Relative paths are still rejected.
* updated the macOS seatbelt path in core so it enforces the same Unix
socket behavior:
  * allowlisted sockets generate explicit network* subpath rules
  * allow-all generates a broad network* (subpath "/") rule

---------

Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
This commit is contained in:
viyatb-oai
2026-02-20 10:56:57 -08:00
committed by GitHub
Unverified
parent 73fd939296
commit 28c0089060
19 changed files with 553 additions and 18 deletions
@@ -13466,6 +13466,12 @@
"null"
]
},
"dangerouslyAllowAllUnixSockets": {
"type": [
"boolean",
"null"
]
},
"dangerouslyAllowNonLoopbackAdmin": {
"type": [
"boolean",
@@ -117,6 +117,12 @@
"null"
]
},
"dangerouslyAllowAllUnixSockets": {
"type": [
"boolean",
"null"
]
},
"dangerouslyAllowNonLoopbackAdmin": {
"type": [
"boolean",
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type NetworkRequirements = { enabled: boolean | null, httpPort: number | null, socksPort: number | null, allowUpstreamProxy: boolean | null, dangerouslyAllowNonLoopbackProxy: boolean | null, dangerouslyAllowNonLoopbackAdmin: boolean | null, allowedDomains: Array<string> | null, deniedDomains: Array<string> | null, allowUnixSockets: Array<string> | null, allowLocalBinding: boolean | null, };
export type NetworkRequirements = { enabled: boolean | null, httpPort: number | null, socksPort: number | null, allowUpstreamProxy: boolean | null, dangerouslyAllowNonLoopbackProxy: boolean | null, dangerouslyAllowNonLoopbackAdmin: boolean | null, dangerouslyAllowAllUnixSockets: boolean | null, allowedDomains: Array<string> | null, deniedDomains: Array<string> | null, allowUnixSockets: Array<string> | null, allowLocalBinding: boolean | null, };
@@ -577,6 +577,7 @@ pub struct NetworkRequirements {
pub allow_upstream_proxy: Option<bool>,
pub dangerously_allow_non_loopback_proxy: Option<bool>,
pub dangerously_allow_non_loopback_admin: Option<bool>,
pub dangerously_allow_all_unix_sockets: Option<bool>,
pub allowed_domains: Option<Vec<String>>,
pub denied_domains: Option<Vec<String>>,
pub allow_unix_sockets: Option<Vec<String>>,
+3
View File
@@ -161,6 +161,7 @@ fn map_network_requirements_to_api(
allow_upstream_proxy: network.allow_upstream_proxy,
dangerously_allow_non_loopback_proxy: network.dangerously_allow_non_loopback_proxy,
dangerously_allow_non_loopback_admin: network.dangerously_allow_non_loopback_admin,
dangerously_allow_all_unix_sockets: network.dangerously_allow_all_unix_sockets,
allowed_domains: network.allowed_domains,
denied_domains: network.denied_domains,
allow_unix_sockets: network.allow_unix_sockets,
@@ -221,6 +222,7 @@ mod tests {
allow_upstream_proxy: Some(false),
dangerously_allow_non_loopback_proxy: Some(false),
dangerously_allow_non_loopback_admin: Some(false),
dangerously_allow_all_unix_sockets: Some(true),
allowed_domains: Some(vec!["api.openai.com".to_string()]),
denied_domains: Some(vec!["example.com".to_string()]),
allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]),
@@ -258,6 +260,7 @@ mod tests {
allow_upstream_proxy: Some(false),
dangerously_allow_non_loopback_proxy: Some(false),
dangerously_allow_non_loopback_admin: Some(false),
dangerously_allow_all_unix_sockets: Some(true),
allowed_domains: Some(vec!["api.openai.com".to_string()]),
denied_domains: Some(vec!["example.com".to_string()]),
allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]),
@@ -135,6 +135,7 @@ pub struct NetworkRequirementsToml {
pub allow_upstream_proxy: Option<bool>,
pub dangerously_allow_non_loopback_proxy: Option<bool>,
pub dangerously_allow_non_loopback_admin: Option<bool>,
pub dangerously_allow_all_unix_sockets: Option<bool>,
pub allowed_domains: Option<Vec<String>>,
pub denied_domains: Option<Vec<String>>,
pub allow_unix_sockets: Option<Vec<String>>,
@@ -150,6 +151,7 @@ pub struct NetworkConstraints {
pub allow_upstream_proxy: Option<bool>,
pub dangerously_allow_non_loopback_proxy: Option<bool>,
pub dangerously_allow_non_loopback_admin: Option<bool>,
pub dangerously_allow_all_unix_sockets: Option<bool>,
pub allowed_domains: Option<Vec<String>>,
pub denied_domains: Option<Vec<String>>,
pub allow_unix_sockets: Option<Vec<String>>,
@@ -165,6 +167,7 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
allow_upstream_proxy,
dangerously_allow_non_loopback_proxy,
dangerously_allow_non_loopback_admin,
dangerously_allow_all_unix_sockets,
allowed_domains,
denied_domains,
allow_unix_sockets,
@@ -177,6 +180,7 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
allow_upstream_proxy,
dangerously_allow_non_loopback_proxy,
dangerously_allow_non_loopback_admin,
dangerously_allow_all_unix_sockets,
allowed_domains,
denied_domains,
allow_unix_sockets,
@@ -1040,6 +1044,7 @@ mod tests {
[experimental_network]
enabled = true
allow_upstream_proxy = false
dangerously_allow_all_unix_sockets = true
allowed_domains = ["api.example.com", "*.openai.com"]
denied_domains = ["blocked.example.com"]
allow_unix_sockets = ["/tmp/example.sock"]
@@ -1058,6 +1063,10 @@ mod tests {
assert_eq!(sourced_network.source, source);
assert_eq!(sourced_network.value.enabled, Some(true));
assert_eq!(sourced_network.value.allow_upstream_proxy, Some(false));
assert_eq!(
sourced_network.value.dangerously_allow_all_unix_sockets,
Some(true)
);
assert_eq!(
sourced_network.value.allowed_domains.as_ref(),
Some(&vec![
+3
View File
@@ -656,6 +656,9 @@
},
"type": "array"
},
"dangerously_allow_all_unix_sockets": {
"type": "boolean"
},
"dangerously_allow_non_loopback_admin": {
"type": "boolean"
},
+1
View File
@@ -2401,6 +2401,7 @@ allowed_domains = ["openai.com"]
allow_upstream_proxy: Some(false),
dangerously_allow_non_loopback_proxy: None,
dangerously_allow_non_loopback_admin: None,
dangerously_allow_all_unix_sockets: None,
mode: None,
allowed_domains: Some(vec!["openai.com".to_string()]),
denied_domains: None,
@@ -178,6 +178,13 @@ impl NetworkProxySpec {
constraints.dangerously_allow_non_loopback_admin =
Some(dangerously_allow_non_loopback_admin);
}
if let Some(dangerously_allow_all_unix_sockets) =
requirements.dangerously_allow_all_unix_sockets
{
config.network.dangerously_allow_all_unix_sockets = dangerously_allow_all_unix_sockets;
constraints.dangerously_allow_all_unix_sockets =
Some(dangerously_allow_all_unix_sockets);
}
if let Some(allowed_domains) = requirements.allowed_domains.clone() {
config.network.allowed_domains = allowed_domains.clone();
constraints.allowed_domains = Some(allowed_domains);
+4
View File
@@ -24,6 +24,7 @@ pub struct NetworkToml {
pub allow_upstream_proxy: Option<bool>,
pub dangerously_allow_non_loopback_proxy: Option<bool>,
pub dangerously_allow_non_loopback_admin: Option<bool>,
pub dangerously_allow_all_unix_sockets: Option<bool>,
#[schemars(with = "Option<NetworkModeSchema>")]
pub mode: Option<NetworkMode>,
pub allowed_domains: Option<Vec<String>>,
@@ -74,6 +75,9 @@ impl NetworkToml {
config.network.dangerously_allow_non_loopback_admin =
dangerously_allow_non_loopback_admin;
}
if let Some(dangerously_allow_all_unix_sockets) = self.dangerously_allow_all_unix_sockets {
config.network.dangerously_allow_all_unix_sockets = dangerously_allow_all_unix_sockets;
}
if let Some(mode) = self.mode {
config.network.mode = mode;
}
+23
View File
@@ -133,6 +133,9 @@ fn apply_network_constraints(network: NetworkToml, constraints: &mut NetworkProx
constraints.dangerously_allow_non_loopback_admin =
Some(dangerously_allow_non_loopback_admin);
}
if let Some(dangerously_allow_all_unix_sockets) = network.dangerously_allow_all_unix_sockets {
constraints.dangerously_allow_all_unix_sockets = Some(dangerously_allow_all_unix_sockets);
}
if let Some(allowed_domains) = network.allowed_domains {
constraints.allowed_domains = Some(allowed_domains);
}
@@ -288,4 +291,24 @@ allowed_domains = ["higher.example.com"]
assert_eq!(config.network.allowed_domains, vec!["higher.example.com"]);
}
#[test]
fn apply_network_constraints_includes_allow_all_unix_sockets_flag() {
let config: toml::Value = toml::from_str(
r#"
[network]
dangerously_allow_all_unix_sockets = true
"#,
)
.expect("network table should parse");
let network = network_tables_from_toml(&config)
.expect("network table should deserialize")
.network
.expect("network table should be present");
let mut constraints = NetworkProxyConstraints::default();
apply_network_constraints(network, &mut constraints);
assert_eq!(constraints.dangerously_allow_all_unix_sockets, Some(true));
}
}
+224 -9
View File
@@ -1,16 +1,18 @@
#![cfg(target_os = "macos")]
use codex_network_proxy::ALLOW_LOCAL_BINDING_ENV_KEY;
use codex_network_proxy::NetworkProxy;
use codex_network_proxy::PROXY_URL_ENV_KEYS;
use codex_network_proxy::has_proxy_url_env_vars;
use codex_network_proxy::proxy_url_env_value;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::ffi::CStr;
use std::path::Path;
use std::path::PathBuf;
use tokio::process::Child;
use tracing::warn;
use url::Url;
use crate::protocol::SandboxPolicy;
@@ -110,34 +112,131 @@ fn proxy_loopback_ports_from_env(env: &HashMap<String, String>) -> Vec<u16> {
ports.into_iter().collect()
}
fn local_binding_enabled(env: &HashMap<String, String>) -> bool {
env.get(ALLOW_LOCAL_BINDING_ENV_KEY).is_some_and(|value| {
let trimmed = value.trim();
trimmed == "1" || trimmed.eq_ignore_ascii_case("true")
})
}
#[derive(Debug, Default)]
struct ProxyPolicyInputs {
ports: Vec<u16>,
has_proxy_config: bool,
allow_local_binding: bool,
unix_domain_socket_policy: UnixDomainSocketPolicy,
}
#[derive(Debug, Clone)]
// Keep allow-all and allowlist modes disjoint so we don't carry ignored state.
enum UnixDomainSocketPolicy {
AllowAll,
Restricted { allowed: Vec<AbsolutePathBuf> },
}
impl Default for UnixDomainSocketPolicy {
fn default() -> Self {
Self::Restricted { allowed: vec![] }
}
}
#[derive(Debug, Clone)]
struct UnixSocketPathParam {
key: String,
path: AbsolutePathBuf,
}
fn proxy_policy_inputs(network: Option<&NetworkProxy>) -> ProxyPolicyInputs {
if let Some(network) = network {
let mut env = HashMap::new();
network.apply_to_env(&mut env);
let unix_domain_socket_policy = if network.dangerously_allow_all_unix_sockets() {
UnixDomainSocketPolicy::AllowAll
} else {
let allowed = network
.allow_unix_sockets()
.iter()
.filter_map(
|socket_path| match normalize_path_for_sandbox(Path::new(socket_path)) {
Some(path) => Some((path.to_string_lossy().to_string(), path)),
None => {
warn!(
"ignoring network.allow_unix_sockets entry because it could not be normalized: {socket_path}"
);
None
}
},
)
.collect::<BTreeMap<_, _>>()
.into_values()
.collect();
UnixDomainSocketPolicy::Restricted { allowed }
};
return ProxyPolicyInputs {
ports: proxy_loopback_ports_from_env(&env),
has_proxy_config: has_proxy_url_env_vars(&env),
allow_local_binding: local_binding_enabled(&env),
allow_local_binding: network.allow_local_binding(),
unix_domain_socket_policy,
};
}
ProxyPolicyInputs::default()
}
fn normalize_path_for_sandbox(path: &Path) -> Option<AbsolutePathBuf> {
// `AbsolutePathBuf::from_absolute_path()` normalizes relative paths against the current
// working directory, so keep the explicit check to avoid silently accepting relative entries.
if !path.is_absolute() {
return None;
}
let absolute_path = AbsolutePathBuf::from_absolute_path(path).ok()?;
let normalized_path = absolute_path
.as_path()
.canonicalize()
.ok()
.and_then(|canonical_path| AbsolutePathBuf::from_absolute_path(canonical_path).ok());
normalized_path.or(Some(absolute_path))
}
fn unix_socket_path_params(proxy: &ProxyPolicyInputs) -> Vec<UnixSocketPathParam> {
let mut deduped_paths: BTreeMap<String, AbsolutePathBuf> = BTreeMap::new();
let UnixDomainSocketPolicy::Restricted { allowed } = &proxy.unix_domain_socket_policy else {
return vec![];
};
for path in allowed {
deduped_paths
.entry(path.to_string_lossy().to_string())
.or_insert_with(|| path.clone());
}
deduped_paths
.into_values()
.enumerate()
.map(|(index, path)| UnixSocketPathParam {
key: format!("UNIX_SOCKET_PATH_{index}"),
path,
})
.collect()
}
fn unix_socket_dir_params(proxy: &ProxyPolicyInputs) -> Vec<(String, PathBuf)> {
unix_socket_path_params(proxy)
.into_iter()
.map(|param| (param.key, param.path.into_path_buf()))
.collect()
}
/// Returns zero or more complete Seatbelt policy lines for unix socket rules.
/// When non-empty, the returned string is newline-terminated so callers can
/// append it directly to larger policy blocks.
fn unix_socket_policy(proxy: &ProxyPolicyInputs) -> String {
if matches!(
proxy.unix_domain_socket_policy,
UnixDomainSocketPolicy::AllowAll
) {
return "(allow network* (subpath \"/\"))\n".to_string();
}
unix_socket_path_params(proxy)
.iter()
.map(|param| format!("(allow network* (subpath (param \"{}\")))\n", param.key))
.collect()
}
fn dynamic_network_policy(
sandbox_policy: &SandboxPolicy,
enforce_managed_network: bool,
@@ -157,6 +256,11 @@ fn dynamic_network_policy(
"(allow network-outbound (remote ip \"localhost:{port}\"))\n"
));
}
let unix_socket_policy = unix_socket_policy(proxy);
if !unix_socket_policy.is_empty() {
policy.push_str("; allow unix domain sockets for local IPC\n");
policy.push_str(&unix_socket_policy);
}
return format!("{policy}{MACOS_SEATBELT_NETWORK_POLICY}");
}
@@ -339,6 +443,7 @@ pub(crate) fn create_seatbelt_command_args_with_extensions(
file_write_dir_params,
unix_socket_params,
macos_dir_params(),
unix_socket_dir_params(&proxy),
seatbelt_extensions.dir_params,
]
.concat();
@@ -404,15 +509,20 @@ fn macos_dir_params() -> Vec<(String, PathBuf)> {
mod tests {
use super::MACOS_SEATBELT_BASE_POLICY;
use super::ProxyPolicyInputs;
use super::UnixDomainSocketPolicy;
use super::create_seatbelt_command_args;
use super::create_seatbelt_command_args_with_extensions;
use super::dynamic_network_policy;
use super::macos_dir_params;
use super::normalize_path_for_sandbox;
use super::unix_socket_dir_params;
use super::unix_socket_policy;
use crate::protocol::SandboxPolicy;
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
use crate::seatbelt_permissions::MacOsAutomationPermission;
use crate::seatbelt_permissions::MacOsPreferencesPermission;
use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::Path;
@@ -430,6 +540,10 @@ mod tests {
);
}
fn absolute_path(path: &str) -> AbsolutePathBuf {
AbsolutePathBuf::from_absolute_path(Path::new(path)).expect("absolute path")
}
#[test]
fn base_policy_allows_node_cpu_sysctls() {
assert!(
@@ -451,6 +565,7 @@ mod tests {
ports: vec![43128, 48081],
has_proxy_config: true,
allow_local_binding: false,
..ProxyPolicyInputs::default()
},
);
@@ -574,6 +689,7 @@ mod tests {
ports: vec![43128],
has_proxy_config: true,
allow_local_binding: true,
..ProxyPolicyInputs::default()
},
);
@@ -610,6 +726,7 @@ mod tests {
ports: vec![],
has_proxy_config: true,
allow_local_binding: false,
..ProxyPolicyInputs::default()
},
);
@@ -638,12 +755,109 @@ mod tests {
ports: vec![],
has_proxy_config: false,
allow_local_binding: false,
..ProxyPolicyInputs::default()
},
);
assert_eq!(policy, "");
}
#[test]
fn create_seatbelt_args_allowlists_unix_socket_paths() {
let policy = dynamic_network_policy(
&SandboxPolicy::new_read_only_policy(),
false,
&ProxyPolicyInputs {
ports: vec![43128],
has_proxy_config: true,
allow_local_binding: false,
unix_domain_socket_policy: UnixDomainSocketPolicy::Restricted {
allowed: vec![absolute_path("/tmp/example.sock")],
},
},
);
assert!(
policy.contains("(allow network* (subpath (param \"UNIX_SOCKET_PATH_0\")))"),
"policy should allow explicitly configured unix sockets:\n{policy}"
);
}
#[test]
fn unix_socket_policy_non_empty_output_is_newline_terminated() {
let allowlist_policy = unix_socket_policy(&ProxyPolicyInputs {
unix_domain_socket_policy: UnixDomainSocketPolicy::Restricted {
allowed: vec![absolute_path("/tmp/example.sock")],
},
..ProxyPolicyInputs::default()
});
assert!(
allowlist_policy.ends_with('\n'),
"allowlist unix socket policy should end with a newline:\n{allowlist_policy}"
);
let allow_all_policy = unix_socket_policy(&ProxyPolicyInputs {
unix_domain_socket_policy: UnixDomainSocketPolicy::AllowAll,
..ProxyPolicyInputs::default()
});
assert!(
allow_all_policy.ends_with('\n'),
"allow-all unix socket policy should end with a newline:\n{allow_all_policy}"
);
}
#[test]
fn unix_socket_dir_params_use_stable_param_names() {
let params = unix_socket_dir_params(&ProxyPolicyInputs {
unix_domain_socket_policy: UnixDomainSocketPolicy::Restricted {
allowed: vec![
absolute_path("/tmp/b.sock"),
absolute_path("/tmp/a.sock"),
absolute_path("/tmp/a.sock"),
],
},
..ProxyPolicyInputs::default()
});
assert_eq!(
params,
vec![
(
"UNIX_SOCKET_PATH_0".to_string(),
PathBuf::from("/tmp/a.sock")
),
(
"UNIX_SOCKET_PATH_1".to_string(),
PathBuf::from("/tmp/b.sock")
),
]
);
}
#[test]
fn normalize_path_for_sandbox_rejects_relative_paths() {
assert_eq!(normalize_path_for_sandbox(Path::new("relative.sock")), None);
}
#[test]
fn create_seatbelt_args_allows_all_unix_sockets_when_enabled() {
let policy = dynamic_network_policy(
&SandboxPolicy::new_read_only_policy(),
false,
&ProxyPolicyInputs {
ports: vec![43128],
has_proxy_config: true,
allow_local_binding: false,
unix_domain_socket_policy: UnixDomainSocketPolicy::AllowAll,
},
);
assert!(
policy.contains("(allow network* (subpath \"/\"))"),
"policy should allow all unix sockets when flag is enabled:\n{policy}"
);
}
#[test]
fn create_seatbelt_args_full_network_with_proxy_is_still_proxy_only() {
let policy = dynamic_network_policy(
@@ -659,6 +873,7 @@ mod tests {
ports: vec![43128],
has_proxy_config: true,
allow_local_binding: false,
..ProxyPolicyInputs::default()
},
);
+8 -2
View File
@@ -47,6 +47,9 @@ allow_local_binding = true
# macOS-only: allows proxying to a unix socket when request includes `x-unix-socket: /path`.
allow_unix_sockets = ["/tmp/example.sock"]
# DANGEROUS (macOS-only): bypasses unix socket allowlisting and permits any
# absolute socket path from `x-unix-socket`.
dangerously_allow_all_unix_sockets = false
```
### 2) Run the proxy
@@ -116,8 +119,9 @@ let handle = proxy.run().await?;
handle.shutdown().await?;
```
When unix socket proxying is enabled, HTTP/admin bind overrides are still clamped to loopback
to avoid turning the proxy into a remote bridge to local daemons.
When unix socket proxying is enabled (`allow_unix_sockets` or
`dangerously_allow_all_unix_sockets`), HTTP/admin bind overrides are still clamped to loopback to
avoid turning the proxy into a remote bridge to local daemons.
### Policy hook (exec-policy mapping)
@@ -176,6 +180,8 @@ what it can reasonably guarantee.
`dangerously_allow_non_loopback_proxy`
- when unix socket proxying is enabled, both listeners are forced to loopback to avoid turning the
proxy into a remote bridge into local daemons.
- `dangerously_allow_all_unix_sockets = true` bypasses the unix socket allowlist entirely (still
macOS-only and absolute-path-only). Use only in tightly controlled environments.
- `enabled` is enforced at runtime; when false the proxy no-ops and does not bind listeners.
Limitations:
+106 -1
View File
@@ -1,10 +1,12 @@
use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde::Serialize;
use std::net::IpAddr;
use std::net::SocketAddr;
use std::path::Path;
use tracing::warn;
use url::Url;
@@ -33,6 +35,8 @@ pub struct NetworkProxySettings {
#[serde(default)]
pub dangerously_allow_non_loopback_admin: bool,
#[serde(default)]
pub dangerously_allow_all_unix_sockets: bool,
#[serde(default)]
pub mode: NetworkMode,
#[serde(default)]
pub allowed_domains: Vec<String>,
@@ -55,6 +59,7 @@ impl Default for NetworkProxySettings {
allow_upstream_proxy: true,
dangerously_allow_non_loopback_proxy: false,
dangerously_allow_non_loopback_admin: false,
dangerously_allow_all_unix_sockets: false,
mode: NetworkMode::default(),
allowed_domains: Vec::new(),
denied_domains: Vec::new(),
@@ -136,7 +141,7 @@ pub(crate) fn clamp_bind_addrs(
cfg.dangerously_allow_non_loopback_admin,
"admin API",
);
if cfg.allow_unix_sockets.is_empty() {
if cfg.allow_unix_sockets.is_empty() && !cfg.dangerously_allow_all_unix_sockets {
return (http_addr, socks_addr, admin_addr);
}
@@ -172,7 +177,49 @@ pub struct RuntimeConfig {
pub admin_addr: SocketAddr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct UnixStyleAbsolutePath(String);
impl UnixStyleAbsolutePath {
fn parse(value: &str) -> Option<Self> {
value.starts_with('/').then(|| Self(value.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ValidatedUnixSocketPath {
Native(AbsolutePathBuf),
UnixStyleAbsolute(UnixStyleAbsolutePath),
}
impl ValidatedUnixSocketPath {
pub(crate) fn parse(socket_path: &str) -> Result<Self> {
let path = Path::new(socket_path);
if path.is_absolute() {
let path = AbsolutePathBuf::from_absolute_path(path)
.with_context(|| format!("failed to normalize unix socket path {socket_path:?}"))?;
return Ok(Self::Native(path));
}
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<()> {
for (index, socket_path) in cfg.network.allow_unix_sockets.iter().enumerate() {
ValidatedUnixSocketPath::parse(socket_path)
.with_context(|| format!("invalid network.allow_unix_sockets[{index}]"))?;
}
Ok(())
}
pub fn resolve_runtime(cfg: &NetworkProxyConfig) -> Result<RuntimeConfig> {
validate_unix_socket_allowlist_paths(cfg)?;
let http_addr = resolve_addr(&cfg.network.proxy_url, 3128)
.with_context(|| format!("invalid network.proxy_url: {}", cfg.network.proxy_url))?;
let socks_addr = resolve_addr(&cfg.network.socks_url, 8081)
@@ -340,6 +387,7 @@ mod tests {
allow_upstream_proxy: true,
dangerously_allow_non_loopback_proxy: false,
dangerously_allow_non_loopback_admin: false,
dangerously_allow_all_unix_sockets: false,
mode: NetworkMode::Full,
allowed_domains: Vec::new(),
denied_domains: Vec::new(),
@@ -526,4 +574,61 @@ mod tests {
assert_eq!(socks_addr, "127.0.0.1:8081".parse::<SocketAddr>().unwrap());
assert_eq!(admin_addr, "127.0.0.1:8080".parse::<SocketAddr>().unwrap());
}
#[test]
fn clamp_bind_addrs_forces_loopback_when_all_unix_sockets_enabled() {
let cfg = NetworkProxySettings {
dangerously_allow_non_loopback_proxy: true,
dangerously_allow_non_loopback_admin: true,
dangerously_allow_all_unix_sockets: true,
..Default::default()
};
let http_addr = "0.0.0.0:3128".parse::<SocketAddr>().unwrap();
let socks_addr = "0.0.0.0:8081".parse::<SocketAddr>().unwrap();
let admin_addr = "0.0.0.0:8080".parse::<SocketAddr>().unwrap();
let (http_addr, socks_addr, admin_addr) =
clamp_bind_addrs(http_addr, socks_addr, admin_addr, &cfg);
assert_eq!(http_addr, "127.0.0.1:3128".parse::<SocketAddr>().unwrap());
assert_eq!(socks_addr, "127.0.0.1:8081".parse::<SocketAddr>().unwrap());
assert_eq!(admin_addr, "127.0.0.1:8080".parse::<SocketAddr>().unwrap());
}
#[test]
fn resolve_runtime_rejects_relative_allow_unix_sockets_entries() {
let cfg = NetworkProxyConfig {
network: NetworkProxySettings {
allow_unix_sockets: vec!["relative.sock".to_string()],
..NetworkProxySettings::default()
},
};
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:#}"
);
}
#[test]
fn resolve_runtime_accepts_unix_style_absolute_allow_unix_sockets_entries() {
let cfg = NetworkProxyConfig {
network: NetworkProxySettings {
allow_unix_sockets: vec!["/private/tmp/example.sock".to_string()],
..NetworkProxySettings::default()
},
};
assert!(
resolve_runtime(&cfg).is_ok(),
"unix-style absolute allow_unix_sockets entry should be accepted"
);
}
}
+2 -2
View File
@@ -378,8 +378,8 @@ async fn http_plain_proxy(
};
// `x-unix-socket` is an escape hatch for talking to local daemons. We keep it tightly scoped:
// macOS-only + explicit allowlist, to avoid turning the proxy into a general local capability
// escalation mechanism.
// macOS-only + explicit allowlist by default, to avoid turning the proxy into a general local
// capability escalation mechanism.
if let Some(unix_socket_header) = req.headers().get("x-unix-socket") {
let socket_path = match unix_socket_header.to_str() {
Ok(value) => value.to_string(),
+21 -1
View File
@@ -206,6 +206,10 @@ impl NetworkProxyBuilder {
socks_addr,
socks_enabled: current_cfg.network.enable_socks5,
allow_local_binding: current_cfg.network.allow_local_binding,
allow_unix_sockets: current_cfg.network.allow_unix_sockets.clone(),
dangerously_allow_all_unix_sockets: current_cfg
.network
.dangerously_allow_all_unix_sockets,
admin_addr,
reserved_listeners,
policy_decider: self.policy_decider,
@@ -240,6 +244,8 @@ pub struct NetworkProxy {
socks_addr: SocketAddr,
socks_enabled: bool,
allow_local_binding: bool,
allow_unix_sockets: Vec<String>,
dangerously_allow_all_unix_sockets: bool,
admin_addr: SocketAddr,
reserved_listeners: Option<Arc<ReservedListeners>>,
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
@@ -419,6 +425,18 @@ impl NetworkProxy {
self.admin_addr
}
pub fn allow_local_binding(&self) -> bool {
self.allow_local_binding
}
pub fn allow_unix_sockets(&self) -> &[String] {
&self.allow_unix_sockets
}
pub fn dangerously_allow_all_unix_sockets(&self) -> bool {
self.dangerously_allow_all_unix_sockets
}
pub fn apply_to_env(&self, env: &mut HashMap<String, String>) {
// Enforce proxying for child processes. We intentionally override existing values so
// command-level environment cannot bypass the managed proxy endpoint.
@@ -441,7 +459,9 @@ impl NetworkProxy {
ensure_rustls_crypto_provider();
if !unix_socket_permissions_supported() {
warn!("allowUnixSockets is macOS-only; requests will be rejected on this platform");
warn!(
"allowUnixSockets and dangerouslyAllowAllUnixSockets are macOS-only; requests will be rejected on this platform"
);
}
let reserved_listeners = self.reserved_listeners.as_ref();
+101 -2
View File
@@ -1,5 +1,6 @@
use crate::config::NetworkMode;
use crate::config::NetworkProxyConfig;
use crate::config::ValidatedUnixSocketPath;
use crate::policy::Host;
use crate::policy::is_loopback_host;
use crate::policy::is_non_public_ip;
@@ -418,6 +419,10 @@ impl NetworkProxyState {
}
let guard = self.state.read().await;
if guard.config.network.dangerously_allow_all_unix_sockets {
return Ok(true);
}
// Normalize the path while keeping the absolute-path requirement explicit.
let requested_abs = match AbsolutePathBuf::from_absolute_path(requested_path) {
Ok(path) => path,
@@ -425,7 +430,16 @@ impl NetworkProxyState {
};
let requested_canonical = std::fs::canonicalize(requested_abs.as_path()).ok();
for allowed in &guard.config.network.allow_unix_sockets {
if allowed == path {
let allowed_path = match ValidatedUnixSocketPath::parse(allowed) {
Ok(ValidatedUnixSocketPath::Native(path)) => path,
Ok(ValidatedUnixSocketPath::UnixStyleAbsolute(_)) => continue,
Err(err) => {
warn!("ignoring invalid network.allow_unix_sockets entry at runtime: {err:#}");
continue;
}
};
if allowed_path.as_path() == requested_abs.as_path() {
return Ok(true);
}
@@ -434,7 +448,7 @@ impl NetworkProxyState {
let Some(requested_canonical) = &requested_canonical else {
continue;
};
if let Ok(allowed_canonical) = std::fs::canonicalize(allowed)
if let Ok(allowed_canonical) = std::fs::canonicalize(allowed_path.as_path())
&& &allowed_canonical == requested_canonical
{
return Ok(true);
@@ -1075,6 +1089,77 @@ mod tests {
assert!(validate_policy_against_constraints(&config, &constraints).is_ok());
}
#[test]
fn validate_policy_against_constraints_disallows_allow_all_unix_sockets_without_managed_opt_in()
{
let constraints = NetworkProxyConstraints {
dangerously_allow_all_unix_sockets: Some(false),
..NetworkProxyConstraints::default()
};
let config = NetworkProxyConfig {
network: NetworkProxySettings {
enabled: true,
dangerously_allow_all_unix_sockets: true,
..NetworkProxySettings::default()
},
};
assert!(validate_policy_against_constraints(&config, &constraints).is_err());
}
#[test]
fn validate_policy_against_constraints_disallows_allow_all_unix_sockets_when_allowlist_is_managed()
{
let constraints = NetworkProxyConstraints {
allow_unix_sockets: Some(vec!["/tmp/allowed.sock".to_string()]),
..NetworkProxyConstraints::default()
};
let config = NetworkProxyConfig {
network: NetworkProxySettings {
enabled: true,
dangerously_allow_all_unix_sockets: true,
..NetworkProxySettings::default()
},
};
assert!(validate_policy_against_constraints(&config, &constraints).is_err());
}
#[test]
fn validate_policy_against_constraints_allows_allow_all_unix_sockets_with_managed_opt_in() {
let constraints = NetworkProxyConstraints {
dangerously_allow_all_unix_sockets: Some(true),
..NetworkProxyConstraints::default()
};
let config = NetworkProxyConfig {
network: NetworkProxySettings {
enabled: true,
dangerously_allow_all_unix_sockets: true,
..NetworkProxySettings::default()
},
};
assert!(validate_policy_against_constraints(&config, &constraints).is_ok());
}
#[test]
fn validate_policy_against_constraints_allows_allow_all_unix_sockets_when_unmanaged() {
let constraints = NetworkProxyConstraints::default();
let config = NetworkProxyConfig {
network: NetworkProxySettings {
enabled: true,
dangerously_allow_all_unix_sockets: true,
..NetworkProxySettings::default()
},
};
assert!(validate_policy_against_constraints(&config, &constraints).is_ok());
}
#[test]
fn compile_globset_is_case_insensitive() {
let patterns = vec!["ExAmPle.CoM".to_string()];
@@ -1172,6 +1257,19 @@ mod tests {
assert!(state.is_unix_socket_allowed(&link_s).await.unwrap());
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn unix_socket_allow_all_flag_bypasses_allowlist() {
let state = network_proxy_state_for_policy(NetworkProxySettings {
allowed_domains: vec!["example.com".to_string()],
dangerously_allow_all_unix_sockets: true,
..NetworkProxySettings::default()
});
assert!(state.is_unix_socket_allowed("/tmp/any.sock").await.unwrap());
assert!(!state.is_unix_socket_allowed("relative.sock").await.unwrap());
}
#[cfg(not(target_os = "macos"))]
#[tokio::test]
async fn unix_socket_allowlist_is_rejected_on_non_macos() {
@@ -1179,6 +1277,7 @@ mod tests {
let state = network_proxy_state_for_policy(NetworkProxySettings {
allowed_domains: vec!["example.com".to_string()],
allow_unix_sockets: vec![socket_path.clone()],
dangerously_allow_all_unix_sockets: true,
..NetworkProxySettings::default()
});
+21
View File
@@ -19,6 +19,7 @@ pub struct NetworkProxyConstraints {
pub allow_upstream_proxy: Option<bool>,
pub dangerously_allow_non_loopback_proxy: Option<bool>,
pub dangerously_allow_non_loopback_admin: Option<bool>,
pub dangerously_allow_all_unix_sockets: Option<bool>,
pub allowed_domains: Option<Vec<String>>,
pub denied_domains: Option<Vec<String>>,
pub allow_unix_sockets: Option<Vec<String>>,
@@ -38,6 +39,7 @@ pub struct PartialNetworkConfig {
pub allow_upstream_proxy: Option<bool>,
pub dangerously_allow_non_loopback_proxy: Option<bool>,
pub dangerously_allow_non_loopback_admin: Option<bool>,
pub dangerously_allow_all_unix_sockets: Option<bool>,
#[serde(default)]
pub allowed_domains: Option<Vec<String>>,
#[serde(default)]
@@ -52,6 +54,7 @@ pub fn build_config_state(
config: NetworkProxyConfig,
constraints: NetworkProxyConstraints,
) -> anyhow::Result<ConfigState> {
crate::config::validate_unix_socket_allowlist_paths(&config)?;
let deny_set = compile_globset(&config.network.denied_domains)?;
let allow_set = compile_globset(&config.network.allowed_domains)?;
Ok(ConfigState {
@@ -173,6 +176,24 @@ pub fn validate_policy_against_constraints(
},
)?;
let allow_all_unix_sockets = constraints
.dangerously_allow_all_unix_sockets
.unwrap_or(constraints.allow_unix_sockets.is_none());
validate(
config.network.dangerously_allow_all_unix_sockets,
move |candidate| {
if *candidate && !allow_all_unix_sockets {
Err(invalid_value(
"network.dangerously_allow_all_unix_sockets",
"true",
"false (disabled by managed config)",
))
} else {
Ok(())
}
},
)?;
if let Some(allow_local_binding) = constraints.allow_local_binding {
validate(config.network.allow_local_binding, move |candidate| {
if *candidate && !allow_local_binding {
+6
View File
@@ -332,6 +332,7 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
allow_upstream_proxy,
dangerously_allow_non_loopback_proxy,
dangerously_allow_non_loopback_admin,
dangerously_allow_all_unix_sockets,
allowed_domains,
denied_domains,
allow_unix_sockets,
@@ -360,6 +361,11 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
"dangerously_allow_non_loopback_admin={dangerously_allow_non_loopback_admin}"
));
}
if let Some(dangerously_allow_all_unix_sockets) = dangerously_allow_all_unix_sockets {
parts.push(format!(
"dangerously_allow_all_unix_sockets={dangerously_allow_all_unix_sockets}"
));
}
if let Some(allowed_domains) = allowed_domains {
parts.push(format!("allowed_domains=[{}]", allowed_domains.join(", ")));
}