mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
Allow exec-server to proxy permitted private IPs upstream (#48568)
## Why Private IP destinations always bypassed inherited upstream proxies, preventing their use for private networks reachable through an upstream VPN proxy. ## What changed - Add `codex exec-server --proxy-private-ips-via-upstream`, also configurable with `CODEX_EXEC_SERVER_PROXY_PRIVATE_IPS_VIA_UPSTREAM=true`. The setting defaults to disabled. - Allow permitted RFC 1918, carrier-grade NAT, and IPv6 unique-local destinations to use an applicable upstream proxy. Loopback and link-local destinations retain direct routing, and destination access policy still applies. - Keep connections direct when no valid upstream proxy applies or `allow_upstream_proxy=false`. Errors after selecting an upstream proxy do not trigger a direct retry. - Rename `ExecServerRuntimePaths` to `ExecServerRuntimeOptions` and carry the routing setting from executor startup into the managed network proxy. ## Testing Add routing coverage for private address ranges, special-use addresses, and public targets with the option enabled and disabled. Verify that HTTP and CONNECT requests still enforce destination allowlists and denylists, and update the CLI help snapshot. GitOrigin-RevId: b7c9cc7da0e0f545694a6521b74c9b36b7b92769
This commit is contained in:
@@ -478,7 +478,7 @@ where
|
||||
}
|
||||
};
|
||||
let proxy = if allow_upstream_proxy {
|
||||
proxy_for_connect(&authority)
|
||||
proxy_for_connect(&authority, &app_state)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1819,6 +1819,55 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn private_ip_upstream_routing_preserves_destination_policy() {
|
||||
for (allowlisted, denied, expected_error) in [
|
||||
(true, true, "blocked-by-denylist"),
|
||||
(false, false, "blocked-by-allowlist"),
|
||||
] {
|
||||
let mut policy = NetworkProxyConfig {
|
||||
allow_local_binding: Some(true),
|
||||
..NetworkProxyConfig::default()
|
||||
};
|
||||
if allowlisted {
|
||||
policy.set_allowed_domains(vec!["100.68.58.50".to_string()]);
|
||||
}
|
||||
if denied {
|
||||
policy.set_denied_domains(vec!["100.68.58.50".to_string()]);
|
||||
}
|
||||
let mut state = network_proxy_state_for_policy(policy);
|
||||
state.set_proxy_private_ips_via_upstream(/*enabled*/ true);
|
||||
let state = Arc::new(state);
|
||||
for method in [Method::GET, Method::CONNECT] {
|
||||
let mut req = Request::builder()
|
||||
.method(method.clone())
|
||||
.uri("http://100.68.58.50:80/api/items")
|
||||
.header("host", "100.68.58.50:80")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
req.extensions_mut().insert(state.clone());
|
||||
let response = if method == Method::CONNECT {
|
||||
http_connect_accept(
|
||||
/*policy_decider*/ None, /*environment_id*/ None, req,
|
||||
)
|
||||
.await
|
||||
.unwrap_err()
|
||||
} else {
|
||||
http_plain_proxy(
|
||||
/*policy_decider*/ None, /*environment_id*/ None, req,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
assert_eq!(
|
||||
response.headers().get("x-proxy-error").unwrap(),
|
||||
expected_error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_plain_proxy_rejects_absolute_uri_host_header_mismatch() {
|
||||
let state = Arc::new(network_proxy_state_for_policy(NetworkProxyConfig::default()));
|
||||
|
||||
Reference in New Issue
Block a user