mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
bca18cba40
## Stack 1. Parent PR: #18240 uses named MITM permissions config. 2. This PR wires managed MITM CA trust into spawned child processes. ## Why When Codex terminates HTTPS for limited mode or MITM hooks, child HTTPS clients need to trust Codex's managed MITM CA. Exporting proxy URLs alone is not enough, but blindly replacing user CA settings would be wrong: it can break custom enterprise/test roots, leak unreadable CA files into generated bundles, or make the child env disagree with its sandbox policy. ## Summary 1. Build immutable managed CA bundles under `$CODEX_HOME/proxy` that include native roots, the managed MITM CA, and only inherited or command-scoped CA bundles the child is allowed to read. 2. Export curated CA env vars alongside managed proxy env vars while preserving user CA override semantics, including nested Codex `SSL_CERT_FILE` precedence. 3. Thread generated CA bundle paths into child sandbox readable roots, including debug sandbox execution, so the exported env vars work inside sandboxed commands. 4. Remove only Codex-generated MITM CA bundle env when a child intentionally drops managed proxying for escalation or no-proxy retry. 5. Document the managed CA bundle behavior and cover env injection, per-child bundle generation, sandbox readable roots, and no-proxy cleanup in tests. ## Validation 1. Ran `just test -p codex-network-proxy`. 2. Ran `just test -p codex-protocol`. 3. Ran `just fix -p codex-network-proxy -p codex-protocol`. 4. Tried focused `codex-core` validation, but the crate currently fails to compile in `core/tests/suite/guardian_review.rs` because an existing `Op::UserInput` initializer is missing `additional_context`. --------- Co-authored-by: Eva Wong <evawong@openai.com>
254 lines
7.9 KiB
Rust
254 lines
7.9 KiB
Rust
use crate::connect_policy::TargetCheckedTcpConnector;
|
|
use crate::state::NetworkProxyState;
|
|
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
|
|
use rama_core::Layer;
|
|
use rama_core::Service;
|
|
use rama_core::error::BoxError;
|
|
use rama_core::error::ErrorExt as _;
|
|
use rama_core::error::OpaqueError;
|
|
use rama_core::extensions::ExtensionsMut;
|
|
use rama_core::extensions::ExtensionsRef;
|
|
use rama_core::service::BoxService;
|
|
use rama_http::Body;
|
|
use rama_http::Request;
|
|
use rama_http::Response;
|
|
use rama_http::layer::version_adapter::RequestVersionAdapter;
|
|
use rama_http_backend::client::HttpClientService;
|
|
use rama_http_backend::client::HttpConnector;
|
|
use rama_http_backend::client::proxy::layer::HttpProxyConnectorLayer;
|
|
use rama_net::address::ProxyAddress;
|
|
use rama_net::client::EstablishedClientConnection;
|
|
use rama_net::http::RequestContext;
|
|
use rama_tls_rustls::client::TlsConnectorDataBuilder;
|
|
use rama_tls_rustls::client::TlsConnectorLayer;
|
|
use std::sync::Arc;
|
|
use std::time::Instant;
|
|
use tracing::info;
|
|
use tracing::warn;
|
|
|
|
#[cfg(target_os = "macos")]
|
|
use rama_unix::client::UnixConnector;
|
|
|
|
#[derive(Clone, Default)]
|
|
struct ProxyConfig {
|
|
http: Option<ProxyAddress>,
|
|
https: Option<ProxyAddress>,
|
|
all: Option<ProxyAddress>,
|
|
}
|
|
|
|
impl ProxyConfig {
|
|
fn from_env() -> Self {
|
|
let http = read_proxy_env(&["HTTP_PROXY", "http_proxy"]);
|
|
let https = read_proxy_env(&["HTTPS_PROXY", "https_proxy"]);
|
|
let all = read_proxy_env(&["ALL_PROXY", "all_proxy"]);
|
|
Self { http, https, all }
|
|
}
|
|
|
|
fn proxy_for_protocol(&self, is_secure: bool) -> Option<ProxyAddress> {
|
|
if is_secure {
|
|
self.https
|
|
.clone()
|
|
.or_else(|| self.http.clone())
|
|
.or_else(|| self.all.clone())
|
|
} else {
|
|
self.http.clone().or_else(|| self.all.clone())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn read_proxy_env(keys: &[&str]) -> Option<ProxyAddress> {
|
|
for key in keys {
|
|
let Ok(value) = std::env::var(key) else {
|
|
continue;
|
|
};
|
|
let value = value.trim();
|
|
if value.is_empty() {
|
|
continue;
|
|
}
|
|
match ProxyAddress::try_from(value) {
|
|
Ok(proxy) => {
|
|
if proxy
|
|
.protocol
|
|
.as_ref()
|
|
.map(rama_net::Protocol::is_http)
|
|
.unwrap_or(true)
|
|
{
|
|
return Some(proxy);
|
|
}
|
|
warn!("ignoring {key}: non-http proxy protocol");
|
|
}
|
|
Err(err) => {
|
|
warn!("ignoring {key}: invalid proxy address ({err})");
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
pub(crate) fn proxy_for_connect() -> Option<ProxyAddress> {
|
|
ProxyConfig::from_env().proxy_for_protocol(/*is_secure*/ true)
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct UpstreamClient {
|
|
connector: BoxService<
|
|
Request<Body>,
|
|
EstablishedClientConnection<HttpClientService<Body>, Request<Body>>,
|
|
BoxError,
|
|
>,
|
|
proxy_config: ProxyConfig,
|
|
}
|
|
|
|
impl UpstreamClient {
|
|
pub(crate) fn direct(state: Arc<NetworkProxyState>) -> Self {
|
|
Self::new(
|
|
ProxyConfig::default(),
|
|
TargetCheckedTcpConnector::new(state),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn from_env_proxy(state: Arc<NetworkProxyState>) -> Self {
|
|
Self::new(
|
|
ProxyConfig::from_env(),
|
|
TargetCheckedTcpConnector::new(state),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn direct_with_allow_local_binding(allow_local_binding: bool) -> Self {
|
|
Self::new(
|
|
ProxyConfig::default(),
|
|
TargetCheckedTcpConnector::from_allow_local_binding(allow_local_binding),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn from_env_proxy_with_allow_local_binding(allow_local_binding: bool) -> Self {
|
|
Self::new(
|
|
ProxyConfig::from_env(),
|
|
TargetCheckedTcpConnector::from_allow_local_binding(allow_local_binding),
|
|
)
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
pub(crate) fn unix_socket(path: &str) -> Self {
|
|
let connector = build_unix_connector(path);
|
|
Self {
|
|
connector,
|
|
proxy_config: ProxyConfig::default(),
|
|
}
|
|
}
|
|
|
|
fn new(proxy_config: ProxyConfig, transport: TargetCheckedTcpConnector) -> Self {
|
|
let connector = build_http_connector(transport);
|
|
Self {
|
|
connector,
|
|
proxy_config,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Service<Request<Body>> for UpstreamClient {
|
|
type Output = Response;
|
|
type Error = OpaqueError;
|
|
|
|
async fn serve(&self, mut req: Request<Body>) -> Result<Self::Output, Self::Error> {
|
|
let request_context = RequestContext::try_from(&req).ok();
|
|
let authority = request_context
|
|
.as_ref()
|
|
.map(|ctx| ctx.host_with_port().to_string())
|
|
.unwrap_or_else(|| "<unknown>".to_string());
|
|
let proxy = self.proxy_config.proxy_for_protocol(
|
|
request_context
|
|
.as_ref()
|
|
.map(|ctx| ctx.protocol.is_secure())
|
|
.unwrap_or(false),
|
|
);
|
|
match proxy.as_ref() {
|
|
Some(proxy) => info!(
|
|
"HTTP upstream route selected (target={authority}, route=upstream_proxy, proxy={})",
|
|
proxy.address
|
|
),
|
|
None => info!("HTTP upstream route selected (target={authority}, route=direct)"),
|
|
}
|
|
if let Some(proxy) = proxy {
|
|
req.extensions_mut().insert(proxy);
|
|
}
|
|
|
|
let uri = req.uri().clone();
|
|
let connect_started_at = Instant::now();
|
|
let EstablishedClientConnection {
|
|
input: mut req,
|
|
conn: http_connection,
|
|
} = match self.connector.serve(req).await {
|
|
Ok(connection) => {
|
|
info!(
|
|
"HTTP upstream connection established (target={authority}, elapsed_ms={})",
|
|
connect_started_at.elapsed().as_millis()
|
|
);
|
|
connection
|
|
}
|
|
Err(err) => {
|
|
warn!(
|
|
"HTTP upstream connection failed (target={authority}, elapsed_ms={})",
|
|
connect_started_at.elapsed().as_millis()
|
|
);
|
|
return Err(OpaqueError::from_boxed(err));
|
|
}
|
|
};
|
|
|
|
req.extensions_mut()
|
|
.extend(http_connection.extensions().clone());
|
|
|
|
let request_started_at = Instant::now();
|
|
match http_connection.serve(req).await {
|
|
Ok(resp) => {
|
|
info!(
|
|
"HTTP upstream response headers received (target={authority}, elapsed_ms={})",
|
|
request_started_at.elapsed().as_millis()
|
|
);
|
|
Ok(resp)
|
|
}
|
|
Err(err) => {
|
|
warn!(
|
|
"HTTP upstream response headers failed (target={authority}, elapsed_ms={})",
|
|
request_started_at.elapsed().as_millis()
|
|
);
|
|
Err(OpaqueError::from_boxed(err)
|
|
.context(format!("http request failure for uri: {uri}")))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn build_http_connector(
|
|
transport: TargetCheckedTcpConnector,
|
|
) -> BoxService<
|
|
Request<Body>,
|
|
EstablishedClientConnection<HttpClientService<Body>, Request<Body>>,
|
|
BoxError,
|
|
> {
|
|
ensure_rustls_crypto_provider();
|
|
let proxy = HttpProxyConnectorLayer::optional().into_layer(transport);
|
|
let tls_config = TlsConnectorDataBuilder::new()
|
|
.with_alpn_protocols_http_auto()
|
|
.build();
|
|
let tls = TlsConnectorLayer::auto()
|
|
.with_connector_data(tls_config)
|
|
.into_layer(proxy);
|
|
let tls = RequestVersionAdapter::new(tls);
|
|
let connector = HttpConnector::new(tls);
|
|
connector.boxed()
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn build_unix_connector(
|
|
path: &str,
|
|
) -> BoxService<
|
|
Request<Body>,
|
|
EstablishedClientConnection<HttpClientService<Body>, Request<Body>>,
|
|
BoxError,
|
|
> {
|
|
let transport = UnixConnector::fixed(path);
|
|
let connector = HttpConnector::new(transport);
|
|
connector.boxed()
|
|
}
|