fix(network-proxy): recheck network proxy connect targets (#19999)

## Why
The proxy checks the requested host before opening the upstream
connection, but DNS can resolve an allowed hostname to a loopback,
private, or other non-public address after that first decision. Without
a final check on the actual socket target, a request that looks
acceptable at the hostname layer can still connect to a local service
once resolution completes.

## What changed
- add a shared TCP connector check for direct proxy egress
- use that path for HTTP, `CONNECT`, SOCKS5, and MITM upstream
connections
- keep configured upstream proxy hops on the existing proxy path
- add direct-connector coverage for allowed and rejected local targets

## Security impact
Direct proxy egress now rechecks the resolved socket address before
connecting, closing the gap between hostname policy evaluation and the
final network target.

## Verification
- `cargo test -p codex-network-proxy`

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
viyatb-oai
2026-04-28 12:51:43 -07:00
committed by GitHub
co-authored by Codex
parent 25ac0e4527
commit e1ba87ccb2
8 changed files with 230 additions and 33 deletions
+16 -13
View File
@@ -1,4 +1,5 @@
use crate::config::NetworkMode;
use crate::connect_policy::TargetCheckedTcpConnector;
use crate::mitm;
use crate::network_policy::BlockDecisionAuditEventArgs;
use crate::network_policy::NetworkDecision;
@@ -66,7 +67,6 @@ use rama_net::proxy::ProxyTarget;
use rama_net::proxy::StreamForwardService;
use rama_net::stream::SocketInfo;
use rama_tcp::client::Request as TcpRequest;
use rama_tcp::client::service::TcpConnector;
use rama_tcp::server::TcpListener;
use rama_tls_rustls::client::TlsConnectorDataBuilder;
use rama_tls_rustls::client::TlsConnectorLayer;
@@ -345,20 +345,22 @@ async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> {
return Ok(());
}
let allow_upstream_proxy = match upgraded
let app_state = match upgraded
.extensions()
.get::<Arc<NetworkProxyState>>()
.cloned()
{
Some(state) => match state.allow_upstream_proxy().await {
Ok(allowed) => allowed,
Err(err) => {
error!("failed to read upstream proxy setting: {err}");
false
}
},
Some(state) => state,
None => {
error!("missing app state");
return Ok(());
}
};
let allow_upstream_proxy = match app_state.allow_upstream_proxy().await {
Ok(allowed) => allowed,
Err(err) => {
error!("failed to read upstream proxy setting: {err}");
false
}
};
@@ -369,7 +371,7 @@ async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> {
None
};
if let Err(err) = forward_connect_tunnel(upgraded, proxy).await {
if let Err(err) = forward_connect_tunnel(upgraded, proxy, app_state).await {
warn!("tunnel error: {err}");
}
Ok(())
@@ -378,6 +380,7 @@ async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> {
async fn forward_connect_tunnel(
upgraded: Upgraded,
proxy: Option<ProxyAddress>,
app_state: Arc<NetworkProxyState>,
) -> Result<(), BoxError> {
let authority = upgraded
.extensions()
@@ -392,7 +395,7 @@ async fn forward_connect_tunnel(
let req = TcpRequest::new_with_extensions(authority.clone(), extensions)
.with_protocol(Protocol::HTTPS);
let proxy_connector = HttpProxyConnector::optional(TcpConnector::new());
let proxy_connector = HttpProxyConnector::optional(TargetCheckedTcpConnector::new(app_state));
let tls_config = TlsConnectorDataBuilder::new()
.with_alpn_protocols_http_auto()
.build();
@@ -730,9 +733,9 @@ async fn http_plain_proxy(
Err(resp) => return Ok(resp),
};
let client = if allow_upstream_proxy {
UpstreamClient::from_env_proxy()
UpstreamClient::from_env_proxy(app_state.clone())
} else {
UpstreamClient::direct()
UpstreamClient::direct(app_state.clone())
};
// Strip hop-by-hop headers only after extracting metadata used for policy correlation.