mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
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:
@@ -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(
|
||||
¤t_url,
|
||||
¤t_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(
|
||||
¤t_route,
|
||||
selected_tls_backend,
|
||||
replay.as_ref(),
|
||||
error,
|
||||
timeout_deadline,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(RouteAwareRequestError::Request(error)) => {
|
||||
self.retry_with_rustls(
|
||||
¤t_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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user