Add system proxy routing to network proxy

This commit is contained in:
canvrno-oai
2026-06-09 14:51:32 -07:00
parent c91497ebef
commit 3aa8bf4bc3
12 changed files with 283 additions and 23 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -3451,6 +3451,7 @@ dependencies = [
"base64 0.22.1",
"chrono",
"clap",
"codex-client",
"codex-utils-absolute-path",
"codex-utils-home-dir",
"codex-utils-rustls-provider",

View File

@@ -31,7 +31,9 @@ pub use crate::outbound_proxy::ClientRouteClass;
pub use crate::outbound_proxy::OutboundProxyConfig;
pub use crate::outbound_proxy::OutboundProxyMode;
pub use crate::outbound_proxy::RouteFailureClass;
pub use crate::outbound_proxy::SystemProxyRouteDecision;
pub use crate::outbound_proxy::build_reqwest_client_for_route;
pub use crate::outbound_proxy::resolve_system_proxy_for_url;
pub use crate::request::PreparedRequestBody;
pub use crate::request::Request;
pub use crate::request::RequestBody;

View File

@@ -109,6 +109,14 @@ impl fmt::Display for RouteFailureClass {
}
}
/// URL-specific system proxy decision from the platform resolver.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SystemProxyRouteDecision {
Direct,
Proxy { url: String },
Unavailable { failure: RouteFailureClass },
}
/// How a resolver-aware client should choose an outbound proxy.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum OutboundProxyMode {
@@ -383,6 +391,36 @@ enum SystemProxyDecision {
Unavailable { failure: RouteFailureClass },
}
impl From<SystemProxyDecision> for SystemProxyRouteDecision {
fn from(decision: SystemProxyDecision) -> Self {
match decision {
SystemProxyDecision::Direct => Self::Direct,
SystemProxyDecision::Proxy { url } => Self::Proxy { url },
SystemProxyDecision::Unavailable { failure } => Self::Unavailable { failure },
}
}
}
/// Resolve system proxy/PAC routing for a single outbound URL.
pub fn resolve_system_proxy_for_url(
request_url: &str,
include_auto_detect: bool,
) -> SystemProxyRouteDecision {
if !SystemProxyEnvOverride::from_env().system_discovery_enabled() || !system_proxy_supported() {
return SystemProxyRouteDecision::Unavailable {
failure: RouteFailureClass::ProxyResolutionUnavailable,
};
}
let Some(origin) = RequestOrigin::parse(request_url) else {
return SystemProxyRouteDecision::Unavailable {
failure: RouteFailureClass::InvalidProxyConfig,
};
};
resolve_system_proxy(request_url, &origin, include_auto_detect).into()
}
fn resolve_system_proxy(
request_url: &str,
origin: &RequestOrigin,

View File

@@ -137,6 +137,7 @@ use crate::config_lock::config_without_lock_controls;
use crate::config_lock::lock_layer_from_config;
use crate::config_lock::read_config_lock_from_path;
use codex_network_proxy::NetworkProxyConfig;
use codex_network_proxy::UpstreamProxyMode;
use toml::Value as TomlValue;
use toml_edit::DocumentMut;
@@ -2483,6 +2484,21 @@ fn resolved_system_proxy_config(
resolved_system_proxy_config_from_features(cfg, features.get(), mode_requirement)
}
fn apply_system_proxy_feature_config_to_network_proxy(
config: &mut NetworkProxyConfig,
system_proxy: Option<&SystemProxyFeatureConfigToml>,
) {
let Some(system_proxy) = system_proxy else {
return;
};
config.network.upstream_proxy_mode = match system_proxy.mode.unwrap_or_default() {
SystemProxyFeatureModeToml::Auto => UpstreamProxyMode::Auto,
SystemProxyFeatureModeToml::Env => UpstreamProxyMode::Env,
SystemProxyFeatureModeToml::System => UpstreamProxyMode::System,
SystemProxyFeatureModeToml::Direct => UpstreamProxyMode::Direct,
};
}
/// Resolves `[features.system_proxy]` for the initial cloud-config bootstrap.
///
/// This runs before cloud-managed config can be fetched, so it can only use
@@ -3081,6 +3097,10 @@ impl Config {
network_proxy,
);
}
apply_system_proxy_feature_config_to_network_proxy(
&mut configured_network_proxy_config,
system_proxy.as_ref(),
);
configured_network_proxy_config.network.enabled = true;
}
let approval_policy_was_explicit =
@@ -3841,6 +3861,10 @@ impl Config {
network_proxy,
);
}
apply_system_proxy_feature_config_to_network_proxy(
&mut configured_network_proxy_config,
self.system_proxy.as_ref(),
);
configured_network_proxy_config.network.enabled = true;
}
configured_network_proxy_config

View File

@@ -18,6 +18,7 @@ async-trait = { workspace = true }
base64 = { workspace = true }
clap = { workspace = true, features = ["derive"] }
chrono = { workspace = true }
codex-client = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-home-dir = { workspace = true }
codex-utils-rustls-provider = { workspace = true }

View File

@@ -116,6 +116,16 @@ pub struct NetworkUnixSocketPermissions {
pub entries: BTreeMap<String, NetworkUnixSocketPermission>,
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum UpstreamProxyMode {
Auto,
#[default]
Env,
System,
Direct,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct NetworkProxySettings {
@@ -129,6 +139,8 @@ pub struct NetworkProxySettings {
pub enable_socks5_udp: bool,
pub allow_upstream_proxy: bool,
#[serde(default)]
pub upstream_proxy_mode: UpstreamProxyMode,
#[serde(default)]
pub dangerously_allow_non_loopback_proxy: bool,
#[serde(default)]
pub dangerously_allow_all_unix_sockets: bool,
@@ -154,6 +166,7 @@ impl Default for NetworkProxySettings {
socks_url: default_socks_url(),
enable_socks5_udp: true,
allow_upstream_proxy: true,
upstream_proxy_mode: UpstreamProxyMode::default(),
dangerously_allow_non_loopback_proxy: false,
dangerously_allow_all_unix_sockets: false,
mode: NetworkMode::default(),
@@ -586,6 +599,7 @@ mod tests {
socks_url: "http://127.0.0.1:8081".to_string(),
enable_socks5_udp: true,
allow_upstream_proxy: true,
upstream_proxy_mode: UpstreamProxyMode::Env,
dangerously_allow_non_loopback_proxy: false,
dangerously_allow_all_unix_sockets: false,
mode: NetworkMode::Full,

View File

@@ -383,7 +383,25 @@ async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> {
};
let proxy = if allow_upstream_proxy {
proxy_for_connect()
let upstream_proxy_mode = match app_state.upstream_proxy_mode().await {
Ok(mode) => mode,
Err(err) => {
error!("failed to read upstream proxy mode: {err}");
return Ok(());
}
};
let target_host = target.host.to_string();
let target_url = https_url_for_host_port(&target_host, target.port);
match proxy_for_connect(&target_url, upstream_proxy_mode) {
Ok(proxy) => proxy,
Err(err) => {
warn!(
"CONNECT upstream proxy resolution failed (host={}, port={}): {err}",
target.host, target.port
);
return Ok(());
}
}
} else {
None
};
@@ -786,7 +804,15 @@ async fn http_plain_proxy(
Err(resp) => return Ok(resp),
};
let client = if allow_upstream_proxy {
UpstreamClient::from_env_proxy(app_state.clone())
let upstream_proxy_mode = match app_state
.upstream_proxy_mode()
.await
.map_err(|err| internal_error("failed to read upstream proxy mode", err))
{
Ok(mode) => mode,
Err(resp) => return Ok(resp),
};
UpstreamClient::from_proxy_mode(app_state.clone(), upstream_proxy_mode)
} else {
UpstreamClient::direct(app_state.clone())
};
@@ -837,6 +863,15 @@ fn client_addr<T: ExtensionsRef>(input: &T) -> Option<String> {
.map(|info| info.peer_addr().to_string())
}
fn https_url_for_host_port(host: &str, port: u16) -> String {
let host = if host.contains(':') && !host.starts_with('[') {
format!("[{host}]")
} else {
host.to_string()
};
format!("https://{host}:{port}/")
}
fn validate_absolute_form_host_header(
req: &Request,
request_ctx: &RequestContext,

View File

@@ -25,6 +25,7 @@ pub use config::NetworkMode;
pub use config::NetworkProxyConfig;
pub use config::NetworkUnixSocketPermission;
pub use config::NetworkUnixSocketPermissions;
pub use config::UpstreamProxyMode;
pub use config::host_and_port_from_network_addr;
pub use mitm_hook::InjectedHeaderConfig;
pub use mitm_hook::MitmHookActionsConfig;

View File

@@ -1,5 +1,6 @@
use crate::certs::ManagedMitmCa;
use crate::config::NetworkMode;
use crate::config::UpstreamProxyMode;
use crate::mitm_hook::HookEvaluation;
use crate::mitm_hook::MitmHookActions;
use crate::policy::normalize_host;
@@ -61,6 +62,7 @@ pub struct MitmState {
pub(crate) struct MitmUpstreamConfig {
pub(crate) allow_upstream_proxy: bool,
pub(crate) upstream_proxy_mode: UpstreamProxyMode,
pub(crate) allow_local_binding: bool,
}
@@ -109,7 +111,10 @@ impl MitmState {
let ca = ManagedMitmCa::load_or_create()?;
let upstream = if config.allow_upstream_proxy {
UpstreamClient::from_env_proxy_with_allow_local_binding(config.allow_local_binding)
UpstreamClient::from_proxy_mode_with_allow_local_binding(
config.upstream_proxy_mode,
config.allow_local_binding,
)
} else {
UpstreamClient::direct_with_allow_local_binding(config.allow_local_binding)
};

View File

@@ -546,6 +546,12 @@ impl NetworkProxyState {
Ok(guard.config.network.allow_upstream_proxy)
}
pub async fn upstream_proxy_mode(&self) -> Result<crate::config::UpstreamProxyMode> {
self.reload_if_needed().await?;
let guard = self.state.read().await;
Ok(guard.config.network.upstream_proxy_mode)
}
pub async fn allow_local_binding(&self) -> Result<bool> {
self.reload_if_needed().await?;
let guard = self.state.read().await;

View File

@@ -76,6 +76,7 @@ pub fn build_config_state(
let mitm = if config.network.mitm {
Some(Arc::new(MitmState::new(MitmUpstreamConfig {
allow_upstream_proxy: config.network.allow_upstream_proxy,
upstream_proxy_mode: config.network.upstream_proxy_mode,
allow_local_binding: config.network.allow_local_binding,
})?))
} else {

View File

@@ -1,5 +1,9 @@
use crate::config::UpstreamProxyMode;
use crate::connect_policy::TargetCheckedTcpConnector;
use crate::state::NetworkProxyState;
use codex_client::RouteFailureClass;
use codex_client::SystemProxyRouteDecision;
use codex_client::resolve_system_proxy_for_url;
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
use rama_core::Layer;
use rama_core::Service;
@@ -23,20 +27,33 @@ use rama_tls_rustls::client::TlsConnectorDataBuilder;
use rama_tls_rustls::client::TlsConnectorLayer;
use std::sync::Arc;
use std::time::Instant;
use thiserror::Error;
use tracing::info;
use tracing::warn;
#[cfg(target_os = "macos")]
use rama_unix::client::UnixConnector;
#[derive(Debug, Error)]
pub(crate) enum UpstreamProxyError {
#[error("system proxy resolution failed: {0}")]
SystemProxyUnavailable(RouteFailureClass),
#[error("system proxy returned invalid proxy URL: {url}")]
InvalidSystemProxyUrl { url: String },
#[error("system proxy returned unsupported proxy protocol: {url}")]
UnsupportedSystemProxyProtocol { url: String },
}
#[derive(Clone, Default)]
struct ProxyConfig {
struct EnvProxyConfig {
http: Option<ProxyAddress>,
https: Option<ProxyAddress>,
all: Option<ProxyAddress>,
}
impl ProxyConfig {
impl EnvProxyConfig {
fn from_env() -> Self {
let http = read_proxy_env(&["HTTP_PROXY", "http_proxy"]);
let https = read_proxy_env(&["HTTPS_PROXY", "https_proxy"]);
@@ -56,6 +73,83 @@ impl ProxyConfig {
}
}
#[derive(Clone)]
struct ProxyConfig {
mode: UpstreamProxyMode,
env: EnvProxyConfig,
}
impl ProxyConfig {
fn direct() -> Self {
Self {
mode: UpstreamProxyMode::Direct,
env: EnvProxyConfig::default(),
}
}
fn from_mode(mode: UpstreamProxyMode) -> Self {
Self {
mode,
env: EnvProxyConfig::from_env(),
}
}
fn proxy_for_url(
&self,
request_url: &str,
is_secure: bool,
) -> Result<Option<ProxyAddress>, UpstreamProxyError> {
match self.mode {
UpstreamProxyMode::Direct => Ok(None),
UpstreamProxyMode::Env => Ok(self.env.proxy_for_protocol(is_secure)),
UpstreamProxyMode::System => {
system_proxy_for_url(request_url, /*include_auto_detect*/ true)
}
UpstreamProxyMode::Auto => {
match system_proxy_for_url(request_url, /*include_auto_detect*/ true) {
Ok(proxy) => Ok(proxy),
Err(err) => {
warn!("system proxy unavailable; falling back to env proxy ({err})");
Ok(self.env.proxy_for_protocol(is_secure))
}
}
}
}
}
}
fn system_proxy_for_url(
request_url: &str,
include_auto_detect: bool,
) -> Result<Option<ProxyAddress>, UpstreamProxyError> {
match resolve_system_proxy_for_url(request_url, include_auto_detect) {
SystemProxyRouteDecision::Direct => Ok(None),
SystemProxyRouteDecision::Proxy { url } => proxy_address_from_system_url(&url).map(Some),
SystemProxyRouteDecision::Unavailable { failure } => {
Err(UpstreamProxyError::SystemProxyUnavailable(failure))
}
}
}
fn proxy_address_from_system_url(proxy_url: &str) -> Result<ProxyAddress, UpstreamProxyError> {
let proxy = ProxyAddress::try_from(proxy_url).map_err(|_| {
UpstreamProxyError::InvalidSystemProxyUrl {
url: proxy_url.to_string(),
}
})?;
if proxy
.protocol
.as_ref()
.map(rama_net::Protocol::is_http)
.unwrap_or(true)
{
return Ok(proxy);
}
Err(UpstreamProxyError::UnsupportedSystemProxyProtocol {
url: proxy_url.to_string(),
})
}
fn read_proxy_env(keys: &[&str]) -> Option<ProxyAddress> {
for key in keys {
let Ok(value) = std::env::var(key) else {
@@ -85,8 +179,11 @@ fn read_proxy_env(keys: &[&str]) -> Option<ProxyAddress> {
None
}
pub(crate) fn proxy_for_connect() -> Option<ProxyAddress> {
ProxyConfig::from_env().proxy_for_protocol(/*is_secure*/ true)
pub(crate) fn proxy_for_connect(
request_url: &str,
mode: UpstreamProxyMode,
) -> Result<Option<ProxyAddress>, UpstreamProxyError> {
ProxyConfig::from_mode(mode).proxy_for_url(request_url, /*is_secure*/ true)
}
#[derive(Clone)]
@@ -101,29 +198,29 @@ pub(crate) struct UpstreamClient {
impl UpstreamClient {
pub(crate) fn direct(state: Arc<NetworkProxyState>) -> Self {
Self::new(
ProxyConfig::default(),
TargetCheckedTcpConnector::new(state),
)
Self::new(ProxyConfig::direct(), TargetCheckedTcpConnector::new(state))
}
pub(crate) fn from_env_proxy(state: Arc<NetworkProxyState>) -> Self {
pub(crate) fn from_proxy_mode(state: Arc<NetworkProxyState>, mode: UpstreamProxyMode) -> Self {
Self::new(
ProxyConfig::from_env(),
ProxyConfig::from_mode(mode),
TargetCheckedTcpConnector::new(state),
)
}
pub(crate) fn direct_with_allow_local_binding(allow_local_binding: bool) -> Self {
Self::new(
ProxyConfig::default(),
ProxyConfig::direct(),
TargetCheckedTcpConnector::from_allow_local_binding(allow_local_binding),
)
}
pub(crate) fn from_env_proxy_with_allow_local_binding(allow_local_binding: bool) -> Self {
pub(crate) fn from_proxy_mode_with_allow_local_binding(
mode: UpstreamProxyMode,
allow_local_binding: bool,
) -> Self {
Self::new(
ProxyConfig::from_env(),
ProxyConfig::from_mode(mode),
TargetCheckedTcpConnector::from_allow_local_binding(allow_local_binding),
)
}
@@ -133,7 +230,7 @@ impl UpstreamClient {
let connector = build_unix_connector(path);
Self {
connector,
proxy_config: ProxyConfig::default(),
proxy_config: ProxyConfig::direct(),
}
}
@@ -156,12 +253,28 @@ impl Service<Request<Body>> for UpstreamClient {
.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),
);
let proxy = match request_context
.as_ref()
.map(|ctx| request_target_url(&req, ctx))
.map(|request_url| {
self.proxy_config.proxy_for_url(
&request_url,
request_context
.as_ref()
.map(|ctx| ctx.protocol.is_secure())
.unwrap_or(false),
)
})
.transpose()
{
Ok(proxy) => proxy.flatten(),
Err(err) => {
warn!("HTTP upstream proxy resolution failed (target={authority}): {err}");
return Err(OpaqueError::from_display(format!(
"upstream proxy resolution failed: {err}"
)));
}
};
match proxy.as_ref() {
Some(proxy) => info!(
"HTTP upstream route selected (target={authority}, route=upstream_proxy, proxy={})",
@@ -219,6 +332,25 @@ impl Service<Request<Body>> for UpstreamClient {
}
}
fn request_target_url(req: &Request<Body>, request_context: &RequestContext) -> String {
if req.uri().scheme_str().is_some() && req.uri().authority().is_some() {
return req.uri().to_string();
}
let scheme = if request_context.protocol.is_secure() {
"https"
} else {
"http"
};
let authority = request_context.host_with_port();
let path = req
.uri()
.path_and_query()
.map(rama_http::uri::PathAndQuery::as_str)
.unwrap_or("/");
format!("{scheme}://{authority}{path}")
}
fn build_http_connector(
transport: TargetCheckedTcpConnector,
) -> BoxService<