Add opt-in sanitized proxy diagnostics

This commit is contained in:
canvrno-oai
2026-06-15 10:59:58 -07:00
parent a2e88abd08
commit ee48d3aa03
7 changed files with 525 additions and 45 deletions

View File

@@ -6,6 +6,7 @@ mod error;
mod outbound_proxy;
mod request;
mod retry;
mod route_diagnostics;
mod sse;
mod telemetry;
mod transport;
@@ -43,6 +44,9 @@ pub use crate::retry::RetryOn;
pub use crate::retry::RetryPolicy;
pub use crate::retry::backoff;
pub use crate::retry::run_with_retry;
pub use crate::route_diagnostics::emit_auth_http_status;
pub use crate::route_diagnostics::emit_auth_network_environment_snapshot;
pub use crate::route_diagnostics::emit_auth_transport_failure;
pub use crate::sse::sse_stream;
pub use crate::telemetry::RequestTelemetry;
pub use crate::transport::ByteStream;

View File

@@ -15,6 +15,8 @@ use std::time::Instant;
use crate::custom_ca::BuildCustomCaTransportError;
use crate::custom_ca::build_reqwest_client_with_custom_ca;
use crate::route_diagnostics::RouteDecisionSource;
use crate::route_diagnostics::RouteDiagnostic;
#[cfg(any(target_os = "windows", target_os = "macos"))]
use sha2::Digest;
#[cfg(any(target_os = "windows", target_os = "macos"))]
@@ -209,6 +211,7 @@ fn configure_proxy_for_route(
let origin = RequestOrigin::parse(request_url);
if config.mode == OutboundProxyMode::Direct {
RouteDiagnostic::direct(route_class, RouteDecisionSource::ConfigDisabled).emit_opt_in();
return Ok(builder.no_proxy());
}
@@ -217,6 +220,7 @@ fn configure_proxy_for_route(
// `NO_PROXY` is an env-derived direct decision; reqwest remains the
// authority for applying the env proxy contract to its own env proxies.
if origin.as_ref().is_some_and(no_proxy_env_matches_origin) {
RouteDiagnostic::direct(route_class, RouteDecisionSource::Env).emit_opt_in();
if config.mode == OutboundProxyMode::Env {
return Ok(builder.no_proxy());
}
@@ -233,6 +237,12 @@ fn configure_proxy_for_route(
}
if !SystemProxyEnvOverride::from_env().system_discovery_enabled() {
RouteDiagnostic::unavailable(
route_class,
RouteDecisionSource::ConfigDisabled,
RouteFailureClass::ProxyResolutionUnavailable,
)
.emit_opt_in();
if config.mode == OutboundProxyMode::System {
return Err(BuildRouteAwareHttpClientError::SystemProxyUnavailable {
route_class,
@@ -248,6 +258,12 @@ fn configure_proxy_for_route(
}
if !system_proxy_supported() {
RouteDiagnostic::unavailable(
route_class,
RouteDecisionSource::UnsupportedPlatform,
RouteFailureClass::ProxyResolutionUnavailable,
)
.emit_opt_in();
if config.mode == OutboundProxyMode::System {
return Err(BuildRouteAwareHttpClientError::SystemProxyUnavailable {
route_class,
@@ -263,6 +279,12 @@ fn configure_proxy_for_route(
}
let Some(origin) = origin.as_ref() else {
RouteDiagnostic::unavailable(
route_class,
RouteDecisionSource::ResolutionError,
RouteFailureClass::InvalidProxyConfig,
)
.emit_opt_in();
return if config.mode == OutboundProxyMode::System {
Err(BuildRouteAwareHttpClientError::SystemProxyUnavailable {
route_class,
@@ -283,9 +305,15 @@ fn configure_proxy_for_route(
OutboundProxyMode::Auto | OutboundProxyMode::System
);
match resolve_system_proxy(request_url, origin, include_auto_detect) {
SystemProxyDecision::Direct => Ok(builder.no_proxy()),
SystemProxyDecision::Proxy { url } => configure_concrete_proxy(builder, route_class, &url),
SystemProxyDecision::Unavailable { failure } => {
SystemProxyDecision::Direct { source } => {
RouteDiagnostic::direct(route_class, source).emit_opt_in();
Ok(builder.no_proxy())
}
SystemProxyDecision::Proxy { source, url } => {
configure_concrete_proxy(builder, route_class, source, &url)
}
SystemProxyDecision::Unavailable { source, failure } => {
RouteDiagnostic::unavailable(route_class, source, failure).emit_opt_in();
if config.mode == OutboundProxyMode::System {
Err(BuildRouteAwareHttpClientError::SystemProxyUnavailable {
route_class,
@@ -310,15 +338,23 @@ const fn system_proxy_supported() -> bool {
fn configure_concrete_proxy(
builder: reqwest::ClientBuilder,
route_class: ClientRouteClass,
source: RouteDecisionSource,
proxy_url: &str,
) -> Result<reqwest::ClientBuilder, BuildRouteAwareHttpClientError> {
let proxy = match reqwest::Proxy::all(proxy_url) {
Ok(proxy) => proxy,
Err(_source) => {
RouteDiagnostic::unavailable(
route_class,
source,
RouteFailureClass::InvalidProxyConfig,
)
.emit_opt_in();
return Err(BuildRouteAwareHttpClientError::InvalidProxyConfig { route_class });
}
};
let proxy = proxy.no_proxy(reqwest::NoProxy::from_env());
RouteDiagnostic::proxy(route_class, source, proxy_url).emit_opt_in();
Ok(builder.proxy(proxy))
}
@@ -338,18 +374,26 @@ fn configure_env_proxy_handling(
&& let Some(proxy_url) = env_proxy_for_origin(origin)
{
if handling == EnvProxyHandling::ResolveConcreteOrNoProxy {
return configure_concrete_proxy(builder, route_class, &proxy_url);
return configure_concrete_proxy(
builder,
route_class,
RouteDecisionSource::Env,
&proxy_url,
);
}
RouteDiagnostic::proxy(route_class, RouteDecisionSource::Env, &proxy_url).emit_opt_in();
return Ok(builder);
}
if conventional_proxy_env_present() {
RouteDiagnostic::direct(route_class, RouteDecisionSource::Env).emit_opt_in();
return match handling {
EnvProxyHandling::ResolveConcreteOrNoProxy => Ok(builder.no_proxy()),
EnvProxyHandling::DelegateToReqwest => Ok(builder),
};
}
RouteDiagnostic::direct(route_class, RouteDecisionSource::Direct).emit_opt_in();
match handling {
EnvProxyHandling::ResolveConcreteOrNoProxy => Ok(builder.no_proxy()),
EnvProxyHandling::DelegateToReqwest => Ok(builder),
@@ -386,17 +430,25 @@ fn default_port_for_scheme(scheme: &str) -> Option<u16> {
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
#[derive(Debug, Clone, PartialEq, Eq)]
enum SystemProxyDecision {
Direct,
Proxy { url: String },
Unavailable { failure: RouteFailureClass },
Direct {
source: RouteDecisionSource,
},
Proxy {
source: RouteDecisionSource,
url: String,
},
Unavailable {
source: RouteDecisionSource,
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 },
SystemProxyDecision::Direct { .. } => Self::Direct,
SystemProxyDecision::Proxy { url, .. } => Self::Proxy { url },
SystemProxyDecision::Unavailable { failure, .. } => Self::Unavailable { failure },
}
}
}
@@ -406,19 +458,37 @@ 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 {
let decision = if !SystemProxyEnvOverride::from_env().system_discovery_enabled() {
SystemProxyDecision::Unavailable {
source: RouteDecisionSource::ConfigDisabled,
failure: RouteFailureClass::ProxyResolutionUnavailable,
};
}
let Some(origin) = RequestOrigin::parse(request_url) else {
return SystemProxyRouteDecision::Unavailable {
}
} else if !system_proxy_supported() {
SystemProxyDecision::Unavailable {
source: RouteDecisionSource::UnsupportedPlatform,
failure: RouteFailureClass::ProxyResolutionUnavailable,
}
} else if let Some(origin) = RequestOrigin::parse(request_url) {
resolve_system_proxy(request_url, &origin, include_auto_detect)
} else {
SystemProxyDecision::Unavailable {
source: RouteDecisionSource::ResolutionError,
failure: RouteFailureClass::InvalidProxyConfig,
};
}
};
resolve_system_proxy(request_url, &origin, include_auto_detect).into()
match &decision {
SystemProxyDecision::Direct { source } => {
RouteDiagnostic::direct(ClientRouteClass::Other, *source).emit_opt_in();
}
SystemProxyDecision::Proxy { source, url } => {
RouteDiagnostic::proxy(ClientRouteClass::Other, *source, url).emit_opt_in();
}
SystemProxyDecision::Unavailable { source, failure } => {
RouteDiagnostic::unavailable(ClientRouteClass::Other, *source, *failure).emit_opt_in();
}
}
decision.into()
}
fn resolve_system_proxy(
@@ -460,6 +530,7 @@ fn resolve_platform_system_proxy(
_include_auto_detect: bool,
) -> SystemProxyDecision {
SystemProxyDecision::Unavailable {
source: RouteDecisionSource::UnsupportedPlatform,
failure: RouteFailureClass::ProxyResolutionUnavailable,
}
}

View File

@@ -6,6 +6,7 @@ use std::time::Instant;
use super::RequestOrigin;
use super::RouteFailureClass;
use super::SystemProxyDecision;
use crate::route_diagnostics::RouteDecisionSource;
use system_configuration::core_foundation::array::CFArray;
use system_configuration::core_foundation::array::CFArrayRef;
use system_configuration::core_foundation::base::CFEqual;
@@ -87,23 +88,31 @@ pub(super) fn resolve(
) -> SystemProxyDecision {
let Some(target_url) = cf_url(request_url) else {
return SystemProxyDecision::Unavailable {
source: RouteDecisionSource::ResolutionError,
failure: RouteFailureClass::InvalidProxyConfig,
};
};
let Some(settings) = system_proxy_settings(include_auto_detect) else {
return SystemProxyDecision::Unavailable {
source: RouteDecisionSource::ResolutionError,
failure: RouteFailureClass::ProxyResolutionUnavailable,
};
};
let Some(proxies) = copy_proxies_for_url(&target_url, &settings) else {
return SystemProxyDecision::Unavailable {
source: RouteDecisionSource::ResolutionError,
failure: RouteFailureClass::ProxyResolutionUnavailable,
};
};
proxy_array_decision(&proxies, &target_url, origin)
proxy_array_decision(
&proxies,
&target_url,
origin,
RouteDecisionSource::MacOsSystem,
)
}
fn system_proxy_settings(include_auto_detect: bool) -> Option<CFDictionary<CFString, CFType>> {
@@ -146,14 +155,19 @@ fn proxy_array_decision(
proxies: &ProxyArray,
target_url: &CFURL,
origin: &RequestOrigin,
source: RouteDecisionSource,
) -> SystemProxyDecision {
let mut saw_unsupported = false;
let mut saw_unavailable = false;
for proxy in proxies {
match proxy_entry_decision(&proxy, target_url, origin) {
ProxyEntryDecision::Direct => return SystemProxyDecision::Direct,
ProxyEntryDecision::Proxy { url } => return SystemProxyDecision::Proxy { url },
match proxy_entry_decision(&proxy, target_url, origin, source) {
ProxyEntryDecision::Direct { source } => {
return SystemProxyDecision::Direct { source };
}
ProxyEntryDecision::Proxy { source, url } => {
return SystemProxyDecision::Proxy { source, url };
}
ProxyEntryDecision::UnsupportedScheme => saw_unsupported = true,
ProxyEntryDecision::Unavailable => saw_unavailable = true,
}
@@ -161,14 +175,16 @@ fn proxy_array_decision(
if saw_unsupported {
SystemProxyDecision::Unavailable {
source,
failure: RouteFailureClass::UnsupportedProxyScheme,
}
} else if saw_unavailable {
SystemProxyDecision::Unavailable {
source,
failure: RouteFailureClass::ProxyResolutionUnavailable,
}
} else {
SystemProxyDecision::Direct
SystemProxyDecision::Direct { source }
}
}
@@ -176,23 +192,24 @@ fn proxy_entry_decision(
proxy: &ProxyDictionary,
target_url: &CFURL,
origin: &RequestOrigin,
source: RouteDecisionSource,
) -> ProxyEntryDecision {
let Some(proxy_type) = cf_string_value(proxy, unsafe { kCFProxyTypeKey }) else {
return ProxyEntryDecision::Unavailable;
};
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeNone }) {
return ProxyEntryDecision::Direct;
return ProxyEntryDecision::Direct { source };
}
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeHTTP }) {
return concrete_proxy_entry(proxy, "http");
return concrete_proxy_entry(proxy, "http", source);
}
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeHTTPS }) {
// CFNetwork's HTTPS proxy type is a tunneling proxy for HTTPS destinations; it does not
// preserve an explicit TLS-to-proxy transport. See https://developer.apple.com/documentation/cfnetwork/kcfproxytypehttps.
return concrete_proxy_entry(proxy, "http");
return concrete_proxy_entry(proxy, "http", source);
}
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeSOCKS }) {
@@ -233,13 +250,19 @@ fn pac_decision(
Err(_) => return ProxyEntryDecision::Unavailable,
};
match proxy_array_decision(&proxies, target_url, origin) {
SystemProxyDecision::Direct => ProxyEntryDecision::Direct,
SystemProxyDecision::Proxy { url } => ProxyEntryDecision::Proxy { url },
match proxy_array_decision(
&proxies,
target_url,
origin,
RouteDecisionSource::MacOsCfNetworkPac,
) {
SystemProxyDecision::Direct { source } => ProxyEntryDecision::Direct { source },
SystemProxyDecision::Proxy { source, url } => ProxyEntryDecision::Proxy { source, url },
SystemProxyDecision::Unavailable {
failure: RouteFailureClass::UnsupportedProxyScheme,
..
} => ProxyEntryDecision::UnsupportedScheme,
SystemProxyDecision::Unavailable { failure: _ } => ProxyEntryDecision::Unavailable,
SystemProxyDecision::Unavailable { .. } => ProxyEntryDecision::Unavailable,
}
}
@@ -325,7 +348,11 @@ struct PacRunLoopState {
result: Option<Result<ProxyArray, RouteFailureClass>>,
}
fn concrete_proxy_entry(proxy: &ProxyDictionary, proxy_scheme: &str) -> ProxyEntryDecision {
fn concrete_proxy_entry(
proxy: &ProxyDictionary,
proxy_scheme: &str,
source: RouteDecisionSource,
) -> ProxyEntryDecision {
let Some(host) = cf_string_value(proxy, unsafe { kCFProxyHostNameKey })
.map(|host| host.to_string())
.filter(|host| !host.is_empty())
@@ -338,7 +365,7 @@ fn concrete_proxy_entry(proxy: &ProxyDictionary, proxy_scheme: &str) -> ProxyEnt
Some(port) if port > 0 => format!("{proxy_scheme}://{host}:{port}"),
_ => format!("{proxy_scheme}://{host}"),
};
ProxyEntryDecision::Proxy { url }
ProxyEntryDecision::Proxy { source, url }
}
fn bracket_ipv6_host(host: &str) -> String {
@@ -395,8 +422,13 @@ fn cf_url(value: &str) -> Option<CFURL> {
}
enum ProxyEntryDecision {
Direct,
Proxy { url: String },
Direct {
source: RouteDecisionSource,
},
Proxy {
source: RouteDecisionSource,
url: String,
},
UnsupportedScheme,
Unavailable,
}

View File

@@ -7,6 +7,7 @@ use super::RouteFailureClass;
use super::SystemProxyDecision;
use super::no_proxy_matches_origin;
use super::parse_proxy_list;
use crate::route_diagnostics::RouteDecisionSource;
use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND;
use windows_sys::Win32::Foundation::FALSE;
use windows_sys::Win32::Foundation::GetLastError;
@@ -56,7 +57,10 @@ pub(super) fn resolve(
let ie_config = match current_user_ie_proxy_config() {
Ok(config) => config,
Err(failure) => {
return SystemProxyDecision::Unavailable { failure };
return SystemProxyDecision::Unavailable {
source: RouteDecisionSource::ResolutionError,
failure,
};
}
};
@@ -80,17 +84,22 @@ pub(super) fn resolve(
.as_deref()
.is_some_and(|bypass| proxy_bypass_matches_origin(bypass, origin))
{
return SystemProxyDecision::Direct;
return SystemProxyDecision::Direct {
source: RouteDecisionSource::WindowsStatic,
};
}
return proxy_list_decision(proxy, origin);
return proxy_list_decision(proxy, origin, RouteDecisionSource::WindowsStatic);
}
if ie_config.auto_config_url.is_some() || (include_auto_detect && ie_config.auto_detect) {
SystemProxyDecision::Unavailable {
source: RouteDecisionSource::WindowsWinHttpPac,
failure: RouteFailureClass::ProxyResolutionUnavailable,
}
} else {
SystemProxyDecision::Direct
SystemProxyDecision::Direct {
source: RouteDecisionSource::Direct,
}
}
}
@@ -130,6 +139,7 @@ fn resolve_with_winhttp_options(
) -> SystemProxyDecision {
let Some(session) = WinHttpSession::open() else {
return SystemProxyDecision::Unavailable {
source: RouteDecisionSource::WindowsWinHttpPac,
failure: classify_winhttp_error(last_error()),
};
};
@@ -150,35 +160,46 @@ fn resolve_with_winhttp_options(
};
if ok == FALSE {
return SystemProxyDecision::Unavailable {
source: RouteDecisionSource::WindowsWinHttpPac,
failure: classify_winhttp_error(last_error()),
};
}
let proxy_info = ProxyInfo::from_raw(proxy_info);
if proxy_info.access_type == WINHTTP_ACCESS_TYPE_NO_PROXY {
return SystemProxyDecision::Direct;
return SystemProxyDecision::Direct {
source: RouteDecisionSource::WindowsWinHttpPac,
};
}
if proxy_info.access_type != WINHTTP_ACCESS_TYPE_NAMED_PROXY {
return SystemProxyDecision::Unavailable {
source: RouteDecisionSource::WindowsWinHttpPac,
failure: RouteFailureClass::ProxyResolutionUnavailable,
};
}
let Some(proxy) = proxy_info.proxy.as_deref() else {
return SystemProxyDecision::Unavailable {
source: RouteDecisionSource::WindowsWinHttpPac,
failure: RouteFailureClass::ProxyResolutionUnavailable,
};
};
proxy_list_decision(proxy, origin)
proxy_list_decision(proxy, origin, RouteDecisionSource::WindowsWinHttpPac)
}
fn proxy_list_decision(proxy_list: &str, origin: &RequestOrigin) -> SystemProxyDecision {
fn proxy_list_decision(
proxy_list: &str,
origin: &RequestOrigin,
source: RouteDecisionSource,
) -> SystemProxyDecision {
match parse_proxy_list(proxy_list, &origin.scheme) {
ParsedProxyListDecision::Direct => SystemProxyDecision::Direct,
ParsedProxyListDecision::Proxy(url) => SystemProxyDecision::Proxy { url },
ParsedProxyListDecision::Direct => SystemProxyDecision::Direct { source },
ParsedProxyListDecision::Proxy(url) => SystemProxyDecision::Proxy { source, url },
ParsedProxyListDecision::UnsupportedScheme => SystemProxyDecision::Unavailable {
source,
failure: RouteFailureClass::UnsupportedProxyScheme,
},
ParsedProxyListDecision::Unavailable => SystemProxyDecision::Unavailable {
source,
failure: RouteFailureClass::ProxyResolutionUnavailable,
},
}

View File

@@ -0,0 +1,321 @@
//! Sanitized, opt-in diagnostics for resolver-aware HTTP clients.
//!
//! Values emitted from this module must not contain request URLs, PAC URLs,
//! proxy hostnames, credentials, headers, tokens, or certificate paths.
use std::fmt;
use crate::outbound_proxy::ClientRouteClass;
use crate::outbound_proxy::RouteFailureClass;
/// Opt-in switch for sanitized network diagnostics.
///
/// Set to `1`, `true`, `on`, or `yes` to emit diagnostic events. The configured
/// value itself is never logged.
const CODEX_NETWORK_DIAGNOSTICS_ENV: &str = "CODEX_NETWORK_DIAGNOSTICS";
const CODEX_SYSTEM_PROXY_ENV: &str = "CODEX_SYSTEM_PROXY";
fn env_flag_enabled(name: &str) -> bool {
std::env::var(name).ok().as_deref().is_some_and(|value| {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "on" | "yes"
)
})
}
/// Returns whether sanitized network diagnostics are enabled for this process.
fn network_diagnostics_enabled() -> bool {
env_flag_enabled(CODEX_NETWORK_DIAGNOSTICS_ENV)
}
fn env_present(name: &str) -> bool {
std::env::var_os(name).is_some_and(|value| !value.is_empty())
}
fn proxy_env_present(upper: &str, lower: &str) -> bool {
env_present(upper) || env_present(lower)
}
fn system_proxy_override_state() -> &'static str {
let disabled = std::env::var(CODEX_SYSTEM_PROXY_ENV)
.ok()
.as_deref()
.is_some_and(|value| {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"off" | "false" | "0" | "no" | "disabled"
)
});
if disabled { "disabled" } else { "default" }
}
/// Emits environment presence bits for an auth operation.
///
/// Proxy values, CA paths, URLs, headers, and tokens are intentionally omitted.
pub fn emit_auth_network_environment_snapshot(operation: &'static str) {
if !network_diagnostics_enabled() {
return;
}
tracing::info!(
target_class = "auth",
operation = operation,
http_proxy_present = proxy_env_present("HTTP_PROXY", "http_proxy"),
https_proxy_present = proxy_env_present("HTTPS_PROXY", "https_proxy"),
all_proxy_present = proxy_env_present("ALL_PROXY", "all_proxy"),
no_proxy_present = proxy_env_present("NO_PROXY", "no_proxy"),
codex_system_proxy = system_proxy_override_state(),
custom_ca_present = env_present("CODEX_CA_CERTIFICATE") || env_present("SSL_CERT_FILE"),
"opt-in auth network diagnostic snapshot"
);
}
fn classify_reqwest_error(error: &reqwest::Error) -> Option<RouteFailureClass> {
if error.is_timeout() {
return Some(RouteFailureClass::ConnectTimeout);
}
if error.status().is_some_and(|status| status.as_u16() == 407) {
return Some(RouteFailureClass::ProxyAuthenticationRequired);
}
let rendered = error.to_string().to_ascii_lowercase();
if rendered.contains("tls") || rendered.contains("certificate") || rendered.contains("cert") {
return Some(RouteFailureClass::TlsError);
}
if error.is_connect() {
return Some(RouteFailureClass::ResolverError);
}
None
}
/// Emits a coarse auth transport failure without the error text or URL.
pub fn emit_auth_transport_failure(operation: &'static str, error: &reqwest::Error) {
if !network_diagnostics_enabled() {
return;
}
let failure = classify_reqwest_error(error)
.map(|failure| failure.to_string())
.unwrap_or_else(|| "other".to_string());
tracing::info!(
target_class = "auth",
operation = operation,
failure = %failure,
is_timeout = error.is_timeout(),
is_connect = error.is_connect(),
status_present = error.status().is_some(),
status = error
.status()
.map(|status| status.as_u16())
.unwrap_or(/*default*/ 0),
"opt-in auth network transport diagnostic"
);
}
/// Emits an auth HTTP status without response content or endpoint details.
pub fn emit_auth_http_status(operation: &'static str, status: reqwest::StatusCode) {
if !network_diagnostics_enabled() {
return;
}
let failure = if status.as_u16() == 407 {
RouteFailureClass::ProxyAuthenticationRequired.to_string()
} else {
"other".to_string()
};
tracing::info!(
target_class = "auth",
operation = operation,
status = status.as_u16(),
failure = %failure,
"opt-in auth network HTTP status diagnostic"
);
}
/// Source that produced a route decision.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RouteDecisionSource {
Env,
ConfigDisabled,
UnsupportedPlatform,
ResolutionError,
#[cfg(target_os = "macos")]
MacOsCfNetworkPac,
#[cfg(target_os = "macos")]
MacOsSystem,
#[cfg(target_os = "windows")]
WindowsWinHttpPac,
#[cfg(target_os = "windows")]
WindowsStatic,
Direct,
}
impl fmt::Display for RouteDecisionSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Env => "env",
Self::ConfigDisabled => "config_disabled",
Self::UnsupportedPlatform => "unsupported_platform",
Self::ResolutionError => "resolution_error",
#[cfg(target_os = "macos")]
Self::MacOsCfNetworkPac => "macos_cfnetwork_pac",
#[cfg(target_os = "macos")]
Self::MacOsSystem => "macos_system",
#[cfg(target_os = "windows")]
Self::WindowsWinHttpPac => "windows_winhttp_pac",
#[cfg(target_os = "windows")]
Self::WindowsStatic => "windows_static",
Self::Direct => "direct",
})
}
}
/// A proxy endpoint rendered without credentials, hostname, path, or query.
#[derive(Clone, PartialEq, Eq)]
struct RedactedProxyEndpoint(String);
impl RedactedProxyEndpoint {
fn parse(input: &str) -> Self {
let Some((scheme, rest)) = input.split_once("://") else {
return Self("<invalid-proxy-url>".to_string());
};
if scheme.is_empty()
|| !scheme
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
{
return Self("<invalid-proxy-url>".to_string());
}
let Some(authority) = rest
.split(['/', '?', '#'])
.next()
.filter(|authority| !authority.is_empty())
else {
return Self("<invalid-proxy-url>".to_string());
};
let host_port = authority
.rsplit_once('@')
.map_or(authority, |(_, tail)| tail);
let port = redacted_port_suffix(host_port).unwrap_or_default();
Self(format!(
"{}://<redacted-host>{port}",
scheme.to_ascii_lowercase()
))
}
}
fn redacted_port_suffix(host_port: &str) -> Option<String> {
if host_port.starts_with('[') {
let end = host_port.find(']')?;
let port = host_port[end + 1..].strip_prefix(':')?;
return (!port.is_empty() && port.bytes().all(|byte| byte.is_ascii_digit()))
.then(|| format!(":{port}"));
}
let (host, port) = host_port.rsplit_once(':')?;
if host.is_empty() || host.contains(':') || port.is_empty() {
return None;
}
port.bytes()
.all(|byte| byte.is_ascii_digit())
.then(|| format!(":{port}"))
}
impl fmt::Debug for RedactedProxyEndpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl fmt::Display for RedactedProxyEndpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum RouteDecision {
Direct,
Proxy(RedactedProxyEndpoint),
Unavailable(RouteFailureClass),
}
impl fmt::Display for RouteDecision {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Direct => f.write_str("direct"),
Self::Proxy(endpoint) => write!(f, "proxy({endpoint})"),
Self::Unavailable(failure) => write!(f, "unavailable({failure})"),
}
}
}
/// One safe diagnostic event for a resolver/client decision.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RouteDiagnostic {
route_class: ClientRouteClass,
source: RouteDecisionSource,
decision: RouteDecision,
}
impl RouteDiagnostic {
pub(crate) fn direct(route_class: ClientRouteClass, source: RouteDecisionSource) -> Self {
Self::new(route_class, source, RouteDecision::Direct)
}
pub(crate) fn proxy(
route_class: ClientRouteClass,
source: RouteDecisionSource,
proxy_url: &str,
) -> Self {
Self::new(
route_class,
source,
RouteDecision::Proxy(RedactedProxyEndpoint::parse(proxy_url)),
)
}
pub(crate) fn unavailable(
route_class: ClientRouteClass,
source: RouteDecisionSource,
failure: RouteFailureClass,
) -> Self {
Self::new(route_class, source, RouteDecision::Unavailable(failure))
}
fn new(
route_class: ClientRouteClass,
source: RouteDecisionSource,
decision: RouteDecision,
) -> Self {
Self {
route_class,
source,
decision,
}
}
/// Emits a sanitized structured event when diagnostics are explicitly enabled.
pub(crate) fn emit_opt_in(&self) {
if !network_diagnostics_enabled() {
return;
}
let failure = match &self.decision {
RouteDecision::Unavailable(failure) => failure.to_string(),
RouteDecision::Direct | RouteDecision::Proxy(_) => "none".to_string(),
};
let custom_ca_configured =
env_present("CODEX_CA_CERTIFICATE") || env_present("SSL_CERT_FILE");
tracing::info!(
route_class = %self.route_class,
source = %self.source,
decision = %self.decision,
failure = %failure,
custom_ca_configured = custom_ca_configured,
"opt-in outbound route diagnostic"
);
}
}
#[cfg(test)]
#[path = "route_diagnostics_tests.rs"]
mod tests;

View File

@@ -0,0 +1,20 @@
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn proxy_endpoint_redacts_credentials_host_path_and_query() {
let endpoint = RedactedProxyEndpoint::parse(
"http://user:secret@proxy.internal.example:8080/pac?token=secret",
);
assert_eq!(endpoint.to_string(), "http://<redacted-host>:8080");
assert!(!format!("{endpoint:?}").contains("secret"));
assert!(!format!("{endpoint}").contains("proxy.internal"));
}
#[test]
fn invalid_proxy_url_is_not_echoed() {
let endpoint = RedactedProxyEndpoint::parse("not a url with password=secret");
assert_eq!(endpoint.to_string(), "<invalid-proxy-url>");
}

View File

@@ -39,6 +39,9 @@ use crate::token_data::parse_chatgpt_jwt_claims;
use base64::Engine;
use chrono::Utc;
use codex_app_server_protocol::AuthMode;
use codex_client::emit_auth_http_status;
use codex_client::emit_auth_network_environment_snapshot;
use codex_client::emit_auth_transport_failure;
use codex_config::types::AuthCredentialsStoreMode;
use codex_utils_template::Template;
use rand::RngCore;
@@ -748,6 +751,7 @@ pub(crate) async fn exchange_code_for_tokens(
}
let token_endpoint = format!("{}/oauth/token", issuer.trim_end_matches('/'));
emit_auth_network_environment_snapshot("oauth_token_exchange");
let client =
build_auth_reqwest_client_with_auth_route_config(&token_endpoint, auth_route_config)?;
info!(
@@ -771,6 +775,7 @@ pub(crate) async fn exchange_code_for_tokens(
let resp = match resp {
Ok(resp) => resp,
Err(error) => {
emit_auth_transport_failure("oauth_token_exchange", &error);
let error = redact_sensitive_error_url(error);
error!(
is_timeout = error.is_timeout(),
@@ -785,6 +790,7 @@ pub(crate) async fn exchange_code_for_tokens(
let status = resp.status();
if !status.is_success() {
emit_auth_http_status("oauth_token_exchange", status);
let body = resp.text().await.map_err(io::Error::other)?;
let detail = parse_token_endpoint_error(&body);
warn!(
@@ -1172,6 +1178,7 @@ pub(crate) async fn obtain_api_key(
access_token: String,
}
let token_endpoint = format!("{}/oauth/token", issuer.trim_end_matches('/'));
emit_auth_network_environment_snapshot("api_key_exchange");
let client =
build_auth_reqwest_client_with_auth_route_config(&token_endpoint, auth_route_config)?;
let resp = client
@@ -1189,9 +1196,13 @@ pub(crate) async fn obtain_api_key(
.await;
let resp = match resp {
Ok(resp) => resp,
Err(error) => return Err(io::Error::other(error)),
Err(error) => {
emit_auth_transport_failure("api_key_exchange", &error);
return Err(io::Error::other(error));
}
};
if !resp.status().is_success() {
emit_auth_http_status("api_key_exchange", resp.status());
return Err(io::Error::other(format!(
"api key exchange failed with status {}",
resp.status()