Retry proxy CONNECT challenges with Windows authentication (#56000)

## Why

HTTPS requests and WebSocket connections on Windows need to authenticate when a proxy rejects `CONNECT` with status `407`.

## What changed

- Retry proxy challenges through the existing Windows authentication helper, using the signed-in user's Negotiate or NTLM credentials. Treat an origin `407` as a normal HTTP response.
- Bridge authenticated tunnels to the HTTP client through destination-specific named pipes, retaining redirect checks and TLS backend fallback without caching those tunnels across destinations.
- Carry explicit CA certificates and client identities into HTTPS proxy TLS configuration, and apply the configured connect timeout when opening authenticated HTTP tunnels.

GitOrigin-RevId: ec7d9168972a7a640350e5348cfca40307390fbf
This commit is contained in:
efrazer-oai
2026-10-21 22:26:16 +00:00
committed by copyberry
parent d5bc4161b6
commit f496b09bd1
10 changed files with 524 additions and 112 deletions
@@ -9,10 +9,16 @@ use futures::future::BoxFuture;
use http::header::PROXY_AUTHORIZATION;
use super::RouteAwareClientPool;
#[cfg(windows)]
use super::RouteAwareClientPoolError;
use super::RouteAwareRequestError;
use super::SelectedTlsBackend;
use crate::HttpResponse;
use crate::OutboundProxyRoute;
#[cfg(windows)]
use crate::RouteFailureClass;
#[cfg(windows)]
use crate::client::TransportClient;
use crate::client::apply_default_headers;
use crate::request_draft::RequestDraft;
use crate::route_aware_redirect::MAX_REDIRECTS;
@@ -22,6 +28,8 @@ use crate::route_aware_redirect::redirect_request;
use crate::route_aware_redirect::redirect_url;
use crate::route_aware_redirect::remove_sensitive_headers;
use crate::tls_backend_fallback::should_retry_with_rustls;
#[cfg(windows)]
use crate::windows_proxy_auth::ProxyPipe;
impl RouteAwareClientPool {
/// Executes requests through shared routing, destination, and redirect checks.
@@ -108,15 +116,7 @@ impl RouteAwareClientPool {
request.headers_mut().remove(PROXY_AUTHORIZATION);
}
previous_route = Some(current_route.clone());
if let Some(timeout_deadline) = timeout_deadline {
let remaining = timeout_deadline
.checked_duration_since(tokio::time::Instant::now())
.ok_or(RouteAwareRequestError::Timeout)?;
if remaining.is_zero() {
return Err(RouteAwareRequestError::Timeout);
}
*request.timeout_mut() = Some(remaining);
}
set_remaining_timeout(&mut request, timeout_deadline)?;
let method = request.method().clone();
let headers = request.headers().clone();
let version = request.version();
@@ -126,28 +126,46 @@ impl RouteAwareClientPool {
client.execute_without_request_logging(request).await
} else {
client.execute(request).await
};
let response = match result {
Ok(response) => response,
Err(error) => {
let result = self
.retry_with_rustls(
&current_url,
&current_route,
selected_tls_backend,
replay.as_ref(),
error,
timeout_deadline,
)
.await;
if follows_redirects_manually
&& let Err(RouteAwareRequestError::Request(error)) = &result
{
client.log_error_summary(&request_method, &request_url, error);
}
result?
}
.map_err(RouteAwareRequestError::from);
let result = match result {
// A failed CONNECT identifies a proxy challenge. An origin 407 is a response.
#[cfg(windows)]
Err(error)
if error.is_connect()
&& error.failure_class()
== Some(RouteFailureClass::ProxyAuthenticationRequired) =>
{
self.retry_windows_proxy(
&current_route,
selected_tls_backend,
replay.as_ref(),
error,
timeout_deadline,
)
.await
}
Err(RouteAwareRequestError::Request(error)) => {
self.retry_with_rustls(
&current_route,
selected_tls_backend,
replay.as_ref(),
#[cfg(windows)]
/*proxy_pipe*/
None,
error,
timeout_deadline,
)
.await
}
result => result,
};
if follows_redirects_manually
&& let Err(RouteAwareRequestError::Request(error)) = &result
{
client.log_error_summary(&request_method, &request_url, error);
}
let response = result?;
let next_request = if follows_redirects_manually && is_redirect(response.status()) {
redirect_url(&response).and_then(|next_url| {
redirect_request(
@@ -202,16 +220,20 @@ impl RouteAwareClientPool {
pub(super) async fn retry_with_rustls(
&self,
current_url: &reqwest::Url,
current_route: &OutboundProxyRoute,
selected_tls_backend: SelectedTlsBackend,
replay: Option<&reqwest::Request>,
#[cfg(windows)] proxy_pipe: Option<&ProxyPipe>,
error: reqwest::Error,
timeout_deadline: Option<tokio::time::Instant>,
) -> Result<reqwest::Response, RouteAwareRequestError> {
let Some(rustls_clients) = self.rustls_clients.as_ref() else {
return Err(error.into());
};
let Some(replay) = replay else {
return Err(error.into());
};
let current_url = replay.url();
if current_url.scheme() != "https"
|| selected_tls_backend == SelectedTlsBackend::RustlsFallback
@@ -220,25 +242,27 @@ impl RouteAwareClientPool {
return Err(error.into());
}
let Some(mut retry_request) = replay.and_then(reqwest::Request::try_clone) else {
let Some(mut retry_request) = replay.try_clone() else {
return Err(error.into());
};
let mut fallback_client = match rustls_clients.client_for_route(current_route) {
let candidate = rustls_clients.client_for_route(current_route);
#[cfg(windows)]
let candidate = match proxy_pipe {
Some(pipe) => Some(self.windows_proxy_client(
current_route,
SelectedTlsBackend::RustlsFallback,
pipe,
)?),
None => candidate,
};
let mut fallback_client = match candidate {
Some(client) => client,
None => self.rustls_client_for_route(current_route)?,
};
fallback_client.request_logging = self.client_builder.request_logging;
if let Some(timeout_deadline) = timeout_deadline {
let remaining = timeout_deadline
.checked_duration_since(tokio::time::Instant::now())
.ok_or(RouteAwareRequestError::Timeout)?;
if remaining.is_zero() {
return Err(RouteAwareRequestError::Timeout);
}
*retry_request.timeout_mut() = Some(remaining);
}
set_remaining_timeout(&mut retry_request, timeout_deadline)?;
let response = if self.follows_redirects_manually() {
fallback_client
@@ -248,12 +272,103 @@ impl RouteAwareClientPool {
fallback_client.execute(retry_request).await
}?;
rustls_clients.remember(current_url, current_route, fallback_client);
tracing::info!(
event.name = "codex.http_client.tls_backend_fallback",
"HTTP client switched to rustls after a TLS protocol negotiation failure"
);
// A Windows auth pipe serves this destination alone, so it cannot enter the route cache.
#[cfg(windows)]
if proxy_pipe.is_some() {
return Ok(response);
}
rustls_clients.remember(current_url, current_route, fallback_client);
Ok(response)
}
#[cfg(windows)]
async fn retry_windows_proxy(
&self,
current_route: &OutboundProxyRoute,
selected_tls_backend: SelectedTlsBackend,
replay: Option<&reqwest::Request>,
error: RouteAwareRequestError,
timeout_deadline: Option<tokio::time::Instant>,
) -> Result<reqwest::Response, RouteAwareRequestError> {
let OutboundProxyRoute::Proxy { url, .. } = current_route else {
return Err(error);
};
let Some(mut request) = replay.and_then(reqwest::Request::try_clone) else {
return Err(error);
};
if request.url().scheme() != "https" {
return Err(error);
}
let pipe = match crate::windows_proxy_auth::pipe_for(
url,
request.url(),
&self.client_builder.tls,
self.client_builder.connect_timeout,
)
.await
{
Ok(pipe) => pipe,
Err(auth_error) if auth_error.kind() == io::ErrorKind::TimedOut => {
return Err(RouteAwareRequestError::Timeout);
}
Err(_) => return Err(error),
};
let client = self.windows_proxy_client(current_route, selected_tls_backend, &pipe)?;
set_remaining_timeout(&mut request, timeout_deadline)?;
// Keep the listener alive through a possible TLS retry.
match client.execute_without_request_logging(request).await {
Ok(response) => Ok(response),
Err(error) => {
self.retry_with_rustls(
current_route,
selected_tls_backend,
replay,
Some(&pipe),
error,
timeout_deadline,
)
.await
}
}
}
#[cfg(windows)]
fn windows_proxy_client(
&self,
route: &OutboundProxyRoute,
backend: SelectedTlsBackend,
pipe: &ProxyPipe,
) -> Result<TransportClient, RouteAwareClientPoolError> {
let mut builder = self
.client_builder
.clone()
.without_redirects()
.with_windows_proxy_pipe(pipe);
if backend == SelectedTlsBackend::RustlsFallback {
builder = builder.with_rustls_tls();
}
builder
.build_for_resolved_route(&self.http_client_factory, self.route_class, route)
.map_err(Into::into)
}
}
fn set_remaining_timeout(
request: &mut reqwest::Request,
deadline: Option<tokio::time::Instant>,
) -> Result<(), RouteAwareRequestError> {
if let Some(deadline) = deadline {
let remaining = deadline
.checked_duration_since(tokio::time::Instant::now())
.filter(|remaining| !remaining.is_zero())
.ok_or(RouteAwareRequestError::Timeout)?;
*request.timeout_mut() = Some(remaining);
}
Ok(())
}