mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
Add yolo blocklist-only network mode
- Add yolo_only_enforce_blocklist to user config, requirements, API schemas, and debug output. - Make full-access proxy runtime enforce denylist-only mode while preserving deny precedence. - Prevent managed allowlist-only requirements from being bypassed by user blocklist-only yolo settings. - Keep managed-config ignore helper test-only so release builds do not ship an env-var bypass.
This commit is contained in:
@@ -9133,6 +9133,12 @@
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"yoloOnlyEnforceBlocklist": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -5881,6 +5881,12 @@
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"yoloOnlyEnforceBlocklist": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -176,6 +176,12 @@
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"yoloOnlyEnforceBlocklist": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -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, dangerouslyAllowAllUnixSockets: 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, dangerouslyAllowAllUnixSockets: boolean | null, yoloOnlyEnforceBlocklist: boolean | null, allowedDomains: Array<string> | null, deniedDomains: Array<string> | null, allowUnixSockets: Array<string> | null, allowLocalBinding: boolean | null, };
|
||||
|
||||
@@ -871,6 +871,7 @@ pub struct NetworkRequirements {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
pub yolo_only_enforce_blocklist: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
|
||||
@@ -231,6 +231,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_all_unix_sockets: network.dangerously_allow_all_unix_sockets,
|
||||
yolo_only_enforce_blocklist: network.yolo_only_enforce_blocklist,
|
||||
allowed_domains: network.allowed_domains,
|
||||
denied_domains: network.denied_domains,
|
||||
allow_unix_sockets: network.allow_unix_sockets,
|
||||
@@ -316,6 +317,7 @@ mod tests {
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: Some(false),
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
yolo_only_enforce_blocklist: Some(true),
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
managed_allowed_domains_only: Some(false),
|
||||
denied_domains: Some(vec!["example.com".to_string()]),
|
||||
@@ -361,6 +363,7 @@ mod tests {
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: Some(false),
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
yolo_only_enforce_blocklist: 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()]),
|
||||
|
||||
@@ -140,6 +140,9 @@ pub struct NetworkRequirementsToml {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
/// When true, yolo/full-access sandbox mode only enforces `denied_domains`
|
||||
/// and allows all other requests through.
|
||||
pub yolo_only_enforce_blocklist: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
/// When true, only managed `allowed_domains` are respected while managed
|
||||
/// network enforcement is active. User allowlist entries are ignored.
|
||||
@@ -158,6 +161,9 @@ pub struct NetworkConstraints {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
/// When true, yolo/full-access sandbox mode only enforces `denied_domains`
|
||||
/// and allows all other requests through.
|
||||
pub yolo_only_enforce_blocklist: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
/// When true, only managed `allowed_domains` are respected while managed
|
||||
/// network enforcement is active. User allowlist entries are ignored.
|
||||
@@ -176,6 +182,7 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
yolo_only_enforce_blocklist,
|
||||
allowed_domains,
|
||||
managed_allowed_domains_only,
|
||||
denied_domains,
|
||||
@@ -189,6 +196,7 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
yolo_only_enforce_blocklist,
|
||||
allowed_domains,
|
||||
managed_allowed_domains_only,
|
||||
denied_domains,
|
||||
|
||||
@@ -955,6 +955,10 @@
|
||||
},
|
||||
"socks_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"yolo_only_enforce_blocklist": {
|
||||
"description": "Only applies in yolo/full-access sandbox mode. When enabled, the network proxy enforces `denied_domains` but allows all other requests through.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -319,6 +319,7 @@ allowed_domains = ["openai.com"]
|
||||
dangerously_allow_non_loopback_proxy: None,
|
||||
dangerously_allow_all_unix_sockets: None,
|
||||
mode: None,
|
||||
yolo_only_enforce_blocklist: None,
|
||||
allowed_domains: Some(vec!["openai.com".to_string()]),
|
||||
denied_domains: None,
|
||||
allow_unix_sockets: None,
|
||||
|
||||
@@ -92,7 +92,7 @@ impl NetworkProxySpec {
|
||||
let hard_deny_allowlist_misses = requirements
|
||||
.as_ref()
|
||||
.is_some_and(Self::managed_allowed_domains_only);
|
||||
let (config, constraints) = if let Some(requirements) = requirements {
|
||||
let (mut config, constraints) = if let Some(requirements) = requirements {
|
||||
Self::apply_requirements(
|
||||
config,
|
||||
&requirements,
|
||||
@@ -102,6 +102,12 @@ impl NetworkProxySpec {
|
||||
} else {
|
||||
(config, NetworkProxyConstraints::default())
|
||||
};
|
||||
// The blocklist-only escape hatch is intentionally scoped to yolo mode,
|
||||
// regardless of whether it came from user config or managed requirements.
|
||||
if !matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) {
|
||||
config.network.yolo_only_enforce_blocklist = false;
|
||||
}
|
||||
|
||||
validate_policy_against_constraints(&config, &constraints).map_err(|err| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
@@ -225,6 +231,15 @@ impl NetworkProxySpec {
|
||||
constraints.dangerously_allow_all_unix_sockets =
|
||||
Some(dangerously_allow_all_unix_sockets);
|
||||
}
|
||||
if let Some(yolo_only_enforce_blocklist) = requirements.yolo_only_enforce_blocklist {
|
||||
config.network.yolo_only_enforce_blocklist = yolo_only_enforce_blocklist;
|
||||
constraints.yolo_only_enforce_blocklist = Some(yolo_only_enforce_blocklist);
|
||||
} else if hard_deny_allowlist_misses {
|
||||
// Managed allowlist-only mode must not be bypassable by a user
|
||||
// opting into blocklist-only yolo behavior.
|
||||
config.network.yolo_only_enforce_blocklist = false;
|
||||
constraints.yolo_only_enforce_blocklist = Some(false);
|
||||
}
|
||||
let managed_allowed_domains = if hard_deny_allowlist_misses {
|
||||
Some(requirements.allowed_domains.clone().unwrap_or_default())
|
||||
} else {
|
||||
|
||||
@@ -175,6 +175,89 @@ fn managed_allowed_domains_only_blocks_all_user_domains_in_full_access_without_m
|
||||
assert!(spec.hard_deny_allowlist_misses);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yolo_only_enforce_blocklist_is_ignored_outside_full_access() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.yolo_only_enforce_blocklist = true;
|
||||
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
None,
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
)
|
||||
.expect("non-yolo modes should load with blocklist-only disabled");
|
||||
|
||||
assert!(!spec.config.network.yolo_only_enforce_blocklist);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_can_enable_yolo_only_enforce_blocklist_in_full_access() {
|
||||
let requirements = NetworkConstraints {
|
||||
yolo_only_enforce_blocklist: Some(true),
|
||||
denied_domains: Some(vec!["blocked.example.com".to_string()]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
NetworkProxyConfig::default(),
|
||||
Some(requirements),
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
)
|
||||
.expect("requirements blocklist-only mode should load in yolo");
|
||||
|
||||
assert!(spec.config.network.yolo_only_enforce_blocklist);
|
||||
assert_eq!(spec.constraints.yolo_only_enforce_blocklist, Some(true));
|
||||
assert_eq!(
|
||||
spec.config.network.denied_domains,
|
||||
vec!["blocked.example.com".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_yolo_only_enforce_blocklist_is_ignored_outside_full_access() {
|
||||
let requirements = NetworkConstraints {
|
||||
yolo_only_enforce_blocklist: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
NetworkProxyConfig::default(),
|
||||
Some(requirements),
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
)
|
||||
.expect("requirements blocklist-only should be disabled outside yolo");
|
||||
|
||||
assert!(!spec.config.network.yolo_only_enforce_blocklist);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_allowed_domains_only_disables_user_blocklist_only_yolo_without_explicit_requirement() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.yolo_only_enforce_blocklist = true;
|
||||
config.network.allowed_domains = vec!["user.example.com".to_string()];
|
||||
|
||||
let requirements = NetworkConstraints {
|
||||
allowed_domains: Some(vec!["managed.example.com".to_string()]),
|
||||
managed_allowed_domains_only: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
)
|
||||
.expect("managed allowlist-only should disable user blocklist-only yolo");
|
||||
|
||||
assert!(!spec.config.network.yolo_only_enforce_blocklist);
|
||||
assert_eq!(spec.constraints.yolo_only_enforce_blocklist, Some(false));
|
||||
assert_eq!(
|
||||
spec.config.network.allowed_domains,
|
||||
vec!["managed.example.com".to_string()]
|
||||
);
|
||||
assert!(spec.hard_deny_allowlist_misses);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_denied_domains_are_a_baseline_for_default_mode() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
|
||||
@@ -69,6 +69,9 @@ pub struct NetworkToml {
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
#[schemars(with = "Option<NetworkModeSchema>")]
|
||||
pub mode: Option<NetworkMode>,
|
||||
/// Only applies in yolo/full-access sandbox mode. When enabled, the network
|
||||
/// proxy enforces `denied_domains` but allows all other requests through.
|
||||
pub yolo_only_enforce_blocklist: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
@@ -114,6 +117,9 @@ impl NetworkToml {
|
||||
if let Some(mode) = self.mode {
|
||||
config.network.mode = mode;
|
||||
}
|
||||
if let Some(yolo_only_enforce_blocklist) = self.yolo_only_enforce_blocklist {
|
||||
config.network.yolo_only_enforce_blocklist = yolo_only_enforce_blocklist;
|
||||
}
|
||||
if let Some(allowed_domains) = self.allowed_domains.as_ref() {
|
||||
config.network.allowed_domains = allowed_domains.clone();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,18 @@ pub(super) async fn load_config_layers_internal(
|
||||
codex_home: &Path,
|
||||
overrides: LoaderOverrides,
|
||||
) -> io::Result<LoadedConfigLayers> {
|
||||
if super::should_ignore_managed_config() {
|
||||
#[cfg(test)]
|
||||
tracing::info!(
|
||||
"Ignoring managed config layers because {} is set",
|
||||
super::CODEX_IGNORE_MANAGED_CONFIG_ENV_VAR
|
||||
);
|
||||
return Ok(LoadedConfigLayers {
|
||||
managed_config: None,
|
||||
managed_config_from_mdm: None,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let LoaderOverrides {
|
||||
managed_config_path,
|
||||
|
||||
@@ -68,6 +68,20 @@ pub const SYSTEM_CONFIG_TOML_FILE_UNIX: &str = "/etc/codex/config.toml";
|
||||
const DEFAULT_PROGRAM_DATA_DIR_WINDOWS: &str = r"C:\ProgramData";
|
||||
|
||||
const DEFAULT_PROJECT_ROOT_MARKERS: &[&str] = &[".git"];
|
||||
#[cfg(test)]
|
||||
/// Local test escape hatch for ignoring managed admin configuration on
|
||||
/// machines with MDM or system managed config installed.
|
||||
pub(super) const CODEX_IGNORE_MANAGED_CONFIG_ENV_VAR: &str = "CODEX_IGNORE_MANAGED_CONFIG";
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn should_ignore_managed_config() -> bool {
|
||||
std::env::var_os(CODEX_IGNORE_MANAGED_CONFIG_ENV_VAR).is_some()
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(super) fn should_ignore_managed_config() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) async fn first_layer_config_error(layers: &ConfigLayerStack) -> Option<ConfigError> {
|
||||
codex_config::first_layer_config_error::<ConfigToml>(layers, CONFIG_TOML_FILE).await
|
||||
@@ -126,13 +140,20 @@ pub async fn load_config_layers_state(
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::load_managed_admin_requirements_toml(
|
||||
&mut config_requirements_toml,
|
||||
overrides
|
||||
.macos_managed_config_requirements_base64
|
||||
.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
if should_ignore_managed_config() {
|
||||
#[cfg(test)]
|
||||
tracing::info!(
|
||||
"Ignoring managed requirements from MDM because {CODEX_IGNORE_MANAGED_CONFIG_ENV_VAR} is set"
|
||||
);
|
||||
} else {
|
||||
macos::load_managed_admin_requirements_toml(
|
||||
&mut config_requirements_toml,
|
||||
overrides
|
||||
.macos_managed_config_requirements_base64
|
||||
.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Honor the system requirements.toml location.
|
||||
let requirements_toml_file = system_requirements_toml_file()?;
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::config_loader::ConfigLoadError;
|
||||
use crate::config_loader::ConfigRequirements;
|
||||
use crate::config_loader::ConfigRequirementsToml;
|
||||
use crate::config_loader::ConfigRequirementsWithSources;
|
||||
use crate::config_loader::NetworkRequirementsToml;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::load_requirements_toml;
|
||||
use crate::config_loader::version_for_toml;
|
||||
@@ -577,6 +578,43 @@ personality = true
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn load_requirements_toml_parses_yolo_only_enforce_blocklist() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let requirements_file = tmp.path().join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_file,
|
||||
r#"
|
||||
[experimental_network]
|
||||
yolo_only_enforce_blocklist = true
|
||||
denied_domains = ["blocked.example.com"]
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
|
||||
load_requirements_toml(&mut config_requirements_toml, &requirements_file).await?;
|
||||
|
||||
assert_eq!(
|
||||
config_requirements_toml.network.as_deref().cloned(),
|
||||
Some(NetworkRequirementsToml {
|
||||
enabled: None,
|
||||
http_port: None,
|
||||
socks_port: None,
|
||||
allow_upstream_proxy: None,
|
||||
dangerously_allow_non_loopback_proxy: None,
|
||||
dangerously_allow_all_unix_sockets: None,
|
||||
yolo_only_enforce_blocklist: Some(true),
|
||||
allowed_domains: None,
|
||||
managed_allowed_domains_only: None,
|
||||
denied_domains: Some(vec!["blocked.example.com".to_string()]),
|
||||
allow_unix_sockets: None,
|
||||
allow_local_binding: None,
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn cloud_requirements_take_precedence_over_mdm_requirements() -> anyhow::Result<()> {
|
||||
|
||||
@@ -33,6 +33,9 @@ allow_upstream_proxy = true
|
||||
# If you want to expose these listeners beyond localhost, you must opt in explicitly.
|
||||
dangerously_allow_non_loopback_proxy = false
|
||||
mode = "full" # default when unset; use "limited" for read-only mode
|
||||
# In yolo/full-access sandbox mode only: ignore the allowlist and local/private-network guard,
|
||||
# enforcing only `denied_domains`.
|
||||
yolo_only_enforce_blocklist = false
|
||||
# When true, HTTPS CONNECT can be terminated so limited-mode method policy still applies.
|
||||
mitm = false
|
||||
# CA cert/key are managed internally under $CODEX_HOME/proxy/ (ca.pem + ca.key).
|
||||
@@ -191,6 +194,9 @@ what it can reasonably guarantee.
|
||||
- Allowlist-first policy: if `allowed_domains` is empty, requests are blocked until an allowlist is configured.
|
||||
- Domain patterns: exact hosts plus scoped wildcards (`*.example.com`, `**.example.com`) are supported; the global `*` wildcard is rejected.
|
||||
- Deny wins: entries in `denied_domains` always override the allowlist.
|
||||
- YOLO blocklist-only mode: with `yolo_only_enforce_blocklist = true` in full-access sandbox mode,
|
||||
the proxy allows requests that are not explicitly denied, including hosts outside the allowlist
|
||||
and local/private destinations. Use only when that broader egress is acceptable.
|
||||
- Local/private network protection: when `allow_local_binding = false`, the proxy blocks loopback
|
||||
and common private/link-local ranges. Explicit allowlisting of local IP literals (or `localhost`)
|
||||
is required to permit them; hostnames that resolve to local/private IPs are still blocked even if
|
||||
|
||||
@@ -34,6 +34,10 @@ pub struct NetworkProxySettings {
|
||||
pub dangerously_allow_all_unix_sockets: bool,
|
||||
#[serde(default)]
|
||||
pub mode: NetworkMode,
|
||||
/// In yolo/full-access sandbox mode, ignore the allowlist and local-network
|
||||
/// guard so only `denied_domains` remains enforced.
|
||||
#[serde(default)]
|
||||
pub yolo_only_enforce_blocklist: bool,
|
||||
#[serde(default)]
|
||||
pub allowed_domains: Vec<String>,
|
||||
#[serde(default)]
|
||||
@@ -57,6 +61,7 @@ impl Default for NetworkProxySettings {
|
||||
dangerously_allow_non_loopback_proxy: false,
|
||||
dangerously_allow_all_unix_sockets: false,
|
||||
mode: NetworkMode::default(),
|
||||
yolo_only_enforce_blocklist: false,
|
||||
allowed_domains: Vec::new(),
|
||||
denied_domains: Vec::new(),
|
||||
allow_unix_sockets: Vec::new(),
|
||||
@@ -371,6 +376,7 @@ mod tests {
|
||||
dangerously_allow_non_loopback_proxy: false,
|
||||
dangerously_allow_all_unix_sockets: false,
|
||||
mode: NetworkMode::Full,
|
||||
yolo_only_enforce_blocklist: false,
|
||||
allowed_domains: Vec::new(),
|
||||
denied_domains: Vec::new(),
|
||||
allow_unix_sockets: Vec::new(),
|
||||
|
||||
@@ -340,12 +340,20 @@ impl NetworkProxyState {
|
||||
Ok(host) => host,
|
||||
Err(_) => return Ok(HostBlockDecision::Blocked(HostBlockReason::NotAllowed)),
|
||||
};
|
||||
let (deny_set, allow_set, allow_local_binding, allowed_domains_empty, allowed_domains) = {
|
||||
let (
|
||||
deny_set,
|
||||
allow_set,
|
||||
allow_local_binding,
|
||||
yolo_only_enforce_blocklist,
|
||||
allowed_domains_empty,
|
||||
allowed_domains,
|
||||
) = {
|
||||
let guard = self.state.read().await;
|
||||
(
|
||||
guard.deny_set.clone(),
|
||||
guard.allow_set.clone(),
|
||||
guard.config.network.allow_local_binding,
|
||||
guard.config.network.yolo_only_enforce_blocklist,
|
||||
guard.config.network.allowed_domains.is_empty(),
|
||||
guard.config.network.allowed_domains.clone(),
|
||||
)
|
||||
@@ -355,11 +363,15 @@ impl NetworkProxyState {
|
||||
|
||||
// Decision order matters:
|
||||
// 1) explicit deny always wins
|
||||
// 2) local/private networking is opt-in (defense-in-depth)
|
||||
// 3) allowlist is enforced when configured
|
||||
// 2) yolo blocklist-only mode bypasses allowlist and local/private guards
|
||||
// 3) local/private networking is opt-in (defense-in-depth)
|
||||
// 4) allowlist is enforced when configured
|
||||
if deny_set.is_match(host_str) {
|
||||
return Ok(HostBlockDecision::Blocked(HostBlockReason::Denied));
|
||||
}
|
||||
if yolo_only_enforce_blocklist {
|
||||
return Ok(HostBlockDecision::Allowed);
|
||||
}
|
||||
|
||||
let is_allowlisted = allow_set.is_match(host_str);
|
||||
if !allow_local_binding {
|
||||
@@ -858,6 +870,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_yolo_only_enforce_blocklist_allows_non_denied_hosts_without_allowlist() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
yolo_only_enforce_blocklist: true,
|
||||
denied_domains: vec!["blocked.example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("blocked.example.com", 80).await.unwrap(),
|
||||
HostBlockDecision::Blocked(HostBlockReason::Denied)
|
||||
);
|
||||
assert_eq!(
|
||||
state.host_blocked("8.8.8.8", 80).await.unwrap(),
|
||||
HostBlockDecision::Allowed
|
||||
);
|
||||
assert_eq!(
|
||||
state.host_blocked("127.0.0.1", 80).await.unwrap(),
|
||||
HostBlockDecision::Allowed
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_allowed_domain_removes_matching_deny_entry() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct NetworkProxyConstraints {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
pub yolo_only_enforce_blocklist: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub allowlist_expansion_enabled: Option<bool>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
@@ -44,6 +45,7 @@ pub struct PartialNetworkConfig {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
pub yolo_only_enforce_blocklist: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
@@ -193,6 +195,23 @@ pub fn validate_policy_against_constraints(
|
||||
},
|
||||
)?;
|
||||
|
||||
if let Some(yolo_only_enforce_blocklist) = constraints.yolo_only_enforce_blocklist {
|
||||
validate(
|
||||
config.network.yolo_only_enforce_blocklist,
|
||||
move |candidate| {
|
||||
if *candidate && !yolo_only_enforce_blocklist {
|
||||
Err(invalid_value(
|
||||
"network.yolo_only_enforce_blocklist",
|
||||
"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 {
|
||||
|
||||
@@ -333,6 +333,7 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
yolo_only_enforce_blocklist,
|
||||
allowed_domains,
|
||||
managed_allowed_domains_only,
|
||||
denied_domains,
|
||||
@@ -362,6 +363,11 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
|
||||
"dangerously_allow_all_unix_sockets={dangerously_allow_all_unix_sockets}"
|
||||
));
|
||||
}
|
||||
if let Some(yolo_only_enforce_blocklist) = yolo_only_enforce_blocklist {
|
||||
parts.push(format!(
|
||||
"yolo_only_enforce_blocklist={yolo_only_enforce_blocklist}"
|
||||
));
|
||||
}
|
||||
if let Some(allowed_domains) = allowed_domains {
|
||||
parts.push(format!("allowed_domains=[{}]", allowed_domains.join(", ")));
|
||||
}
|
||||
|
||||
@@ -333,6 +333,7 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
yolo_only_enforce_blocklist,
|
||||
allowed_domains,
|
||||
managed_allowed_domains_only,
|
||||
denied_domains,
|
||||
@@ -362,6 +363,11 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
|
||||
"dangerously_allow_all_unix_sockets={dangerously_allow_all_unix_sockets}"
|
||||
));
|
||||
}
|
||||
if let Some(yolo_only_enforce_blocklist) = yolo_only_enforce_blocklist {
|
||||
parts.push(format!(
|
||||
"yolo_only_enforce_blocklist={yolo_only_enforce_blocklist}"
|
||||
));
|
||||
}
|
||||
if let Some(allowed_domains) = allowed_domains {
|
||||
parts.push(format!("allowed_domains=[{}]", allowed_domains.join(", ")));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user