feat(network-proxy): structured policy signaling and attempt correlation to core (#11662)

## Summary
When network requests were blocked, downstream code often had to infer
ask vs deny from free-form response text. That was brittle and led to
incorrect approval behavior.
This PR fixes the proxy side so blocked decisions are structured and
request metadata survives reliably.

## Description
- Blocked proxy responses now carry consistent structured policy
decision data.
- Request attempt metadata is preserved across proxy env paths
(including ALL_PROXY flows).
- Header stripping was tightened so we still remove unsafe forwarding
headers, but keep metadata needed for policy handling.
- Block messages were clarified (for example, allowlist miss vs explicit
deny).
- Added unified violation log entries so policy failures can be
inspected in one place.
- Added/updated tests for these behaviors.

---------

Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
This commit is contained in:
viyatb-oai
2026-02-13 01:01:11 -08:00
committed by GitHub
parent fca5629e34
commit 2bced810da
14 changed files with 581 additions and 83 deletions

View File

@@ -1,7 +1,9 @@
use crate::admin;
use crate::config;
use crate::http_proxy;
use crate::metadata::proxy_username_for_attempt_id;
use crate::network_policy::NetworkPolicyDecider;
use crate::runtime::BlockedRequest;
use crate::runtime::unix_socket_permissions_supported;
use crate::socks5;
use crate::state::NetworkProxyState;
@@ -312,8 +314,12 @@ fn apply_proxy_env_overrides(
socks_addr: SocketAddr,
socks_enabled: bool,
allow_local_binding: bool,
network_attempt_id: Option<&str>,
) {
let http_proxy_url = format!("http://{http_addr}");
let http_proxy_url = network_attempt_id
.map(proxy_username_for_attempt_id)
.map(|username| format!("http://{username}@{http_addr}"))
.unwrap_or_else(|| format!("http://{http_addr}"));
let socks_proxy_url = format!("socks5h://{socks_addr}");
env.insert(
ALLOW_LOCAL_BINDING_ENV_KEY.to_string(),
@@ -354,18 +360,25 @@ fn apply_proxy_env_overrides(
env.insert("ELECTRON_GET_USE_PROXY".to_string(), "true".to_string());
if socks_enabled {
// Keep HTTP_PROXY/HTTPS_PROXY as HTTP endpoints. A lot of clients break if
// those vars contain SOCKS URLs. We only switch ALL_PROXY here.
//
// For attempt-scoped runs, point ALL_PROXY at the HTTP proxy URL so the
// attempt metadata survives in proxy credentials for correlation.
if socks_enabled && network_attempt_id.is_none() {
set_env_keys(env, ALL_PROXY_ENV_KEYS, &socks_proxy_url);
set_env_keys(env, FTP_PROXY_ENV_KEYS, &socks_proxy_url);
#[cfg(target_os = "macos")]
{
// Preserve existing SSH wrappers (for example: Secretive/Teleport setups)
// and only provide a SOCKS ProxyCommand fallback when one is not present.
env.entry("GIT_SSH_COMMAND".to_string())
.or_insert_with(|| format!("ssh -o ProxyCommand='nc -X 5 -x {socks_addr} %h %p'"));
}
} else {
set_env_keys(env, ALL_PROXY_ENV_KEYS, &http_proxy_url);
set_env_keys(env, FTP_PROXY_ENV_KEYS, &http_proxy_url);
}
#[cfg(target_os = "macos")]
if socks_enabled {
// Preserve existing SSH wrappers (for example: Secretive/Teleport setups)
// and only provide a SOCKS ProxyCommand fallback when one is not present.
env.entry("GIT_SSH_COMMAND".to_string())
.or_insert_with(|| format!("ssh -o ProxyCommand='nc -X 5 -x {socks_addr} %h %p'"));
}
}
@@ -386,7 +399,22 @@ impl NetworkProxy {
self.admin_addr
}
pub async fn latest_blocked_request_for_attempt(
&self,
attempt_id: &str,
) -> Result<Option<BlockedRequest>> {
self.state.latest_blocked_for_attempt(attempt_id).await
}
pub fn apply_to_env(&self, env: &mut HashMap<String, String>) {
self.apply_to_env_for_attempt(env, None);
}
pub fn apply_to_env_for_attempt(
&self,
env: &mut HashMap<String, String>,
network_attempt_id: Option<&str>,
) {
// Enforce proxying for child processes. We intentionally override existing values so
// command-level environment cannot bypass the managed proxy endpoint.
apply_proxy_env_overrides(
@@ -395,6 +423,7 @@ impl NetworkProxy {
self.socks_addr,
self.socks_enabled,
self.allow_local_binding,
network_attempt_id,
);
}
@@ -694,6 +723,7 @@ mod tests {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
true,
false,
None,
);
assert_eq!(
@@ -736,6 +766,7 @@ mod tests {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
false,
true,
None,
);
assert_eq!(
@@ -745,6 +776,39 @@ mod tests {
assert_eq!(env.get(ALLOW_LOCAL_BINDING_ENV_KEY), Some(&"1".to_string()));
}
#[test]
fn apply_proxy_env_overrides_embeds_attempt_id_in_http_proxy_url() {
let mut env = HashMap::new();
apply_proxy_env_overrides(
&mut env,
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
true,
false,
Some("attempt-123"),
);
assert_eq!(
env.get("HTTP_PROXY"),
Some(&"http://codex-net-attempt-attempt-123@127.0.0.1:3128".to_string())
);
assert_eq!(
env.get("HTTPS_PROXY"),
Some(&"http://codex-net-attempt-attempt-123@127.0.0.1:3128".to_string())
);
assert_eq!(
env.get("ALL_PROXY"),
Some(&"http://codex-net-attempt-attempt-123@127.0.0.1:3128".to_string())
);
#[cfg(target_os = "macos")]
assert_eq!(
env.get("GIT_SSH_COMMAND"),
Some(&"ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:8081 %h %p'".to_string())
);
#[cfg(not(target_os = "macos"))]
assert_eq!(env.get("GIT_SSH_COMMAND"), None);
}
#[cfg(target_os = "macos")]
#[test]
fn apply_proxy_env_overrides_preserves_existing_git_ssh_command() {
@@ -759,6 +823,7 @@ mod tests {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
true,
false,
None,
);
assert_eq!(