Add a Windows proxy authentication tunnel helper (#55994)

## What changed

Expose `connect_windows_proxy` for callers handling a CONNECT `407` response. Use libcurl with the signed-in Windows user's Negotiate or NTLM credentials, then return the authenticated connection with buffered data preserved.

Support HTTP and HTTPS proxies, with default or caller-provided TLS configuration. HTTPS proxies requiring authentication tokens bound to their TLS connection are unsupported.

Enable libcurl's NTLM support in Cargo and Bazel builds.

## Testing

Add a Windows-only test asserting that libcurl provides NTLM, SPNEGO, and SSPI support.

GitOrigin-RevId: 4ed7d2208766404b6125ce2f4b5f91de5616a37f
This commit is contained in:
efrazer-oai
2026-10-21 21:50:55 +00:00
committed by copyberry
parent d51e9c935f
commit d5bc4161b6
8 changed files with 347 additions and 0 deletions
+5
View File
@@ -385,6 +385,11 @@ crate.from_cargo(
bazel_dep(name = "zstd", version = "1.5.7")
crate.annotation(
# Keep Windows proxy authentication aligned with codex-rs/.cargo/config.toml.
build_script_env = {"CFLAGS": "-DCURL_ENABLE_NTLM"},
crate = "curl-sys",
)
crate.annotation(
# Daemon tests repeatedly hash large CLI binaries, even in fastbuild mode.
# Optimize the native backends as well as the Rust implementation.
+3
View File
@@ -1205,6 +1205,8 @@
"ctor_1.0.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"libc-print\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"link-section\",\"optional\":true,\"req\":\"^0.17.0\"},{\"features\":[\"ctor\"],\"name\":\"linktime-proc-macro\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"macrotest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2\"}],\"features\":{\"default\":[\"std\",\"proc_macro\",\"priority\"],\"priority\":[\"dep:link-section\"],\"proc_macro\":[\"dep:linktime-proc-macro\"],\"std\":[]}}",
"ctr_0.9.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"aes\",\"req\":\"^0.8\"},{\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3.3\"},{\"kind\":\"dev\",\"name\":\"kuznyechik\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"magma\",\"req\":\"^0.8\"}],\"features\":{\"alloc\":[\"cipher/alloc\"],\"block-padding\":[\"cipher/block-padding\"],\"std\":[\"cipher/std\",\"alloc\"],\"zeroize\":[\"cipher/zeroize\"]}}",
"ctutils_0.4.2": "{\"dependencies\":[{\"name\":\"cmov\",\"req\":\"^0.5.3\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.11\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"}],\"features\":{\"alloc\":[],\"subtle\":[\"dep:subtle\"]}}",
"curl-sys_0.4.90+curl-8.21.0": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0\"},{\"name\":\"libc\",\"req\":\"^0.2.2\"},{\"name\":\"libnghttp2-sys\",\"optional\":true,\"req\":\"^0.1.3\"},{\"default_features\":false,\"features\":[\"libc\"],\"name\":\"libz-sys\",\"req\":\"^1.0.18\"},{\"name\":\"openssl-sys\",\"optional\":true,\"req\":\"^0.9.64\",\"target\":\"cfg(all(unix))\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.3\"},{\"features\":[\"no_log_capture\"],\"name\":\"rustls-ffi\",\"optional\":true,\"req\":\"^0.15\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2\",\"target\":\"cfg(target_env = \\\"msvc\\\")\"},{\"features\":[\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.59.0, <0.62.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"ssl\"],\"force-system-lib-on-osx\":[],\"http2\":[\"libnghttp2-sys\"],\"mesalink\":[],\"ntlm\":[],\"poll_7_68_0\":[],\"protocol-ftp\":[],\"rustls\":[\"rustls-ffi\"],\"spnego\":[],\"ssl\":[\"openssl-sys\"],\"static-curl\":[],\"static-ssl\":[\"openssl-sys/vendored\"],\"upkeep_7_62_0\":[],\"windows-static-ssl\":[],\"zlib-ng-compat\":[\"libz-sys/zlib-ng\",\"static-curl\"]}}",
"curl_0.4.50": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.31\"},{\"default_features\":false,\"name\":\"curl-sys\",\"req\":\"^0.4.89\"},{\"name\":\"libc\",\"req\":\"^0.2.42\"},{\"kind\":\"dev\",\"name\":\"mio\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"mio-extras\",\"req\":\"^2.0.3\"},{\"name\":\"openssl-probe\",\"optional\":true,\"req\":\"^0.1.2\",\"target\":\"cfg(all(unix))\"},{\"name\":\"openssl-sys\",\"optional\":true,\"req\":\"^0.9.64\",\"target\":\"cfg(all(unix))\"},{\"name\":\"schannel\",\"req\":\"^0.1.13\",\"target\":\"cfg(target_env = \\\"msvc\\\")\"},{\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_LibraryLoader\",\"Win32_Security_Cryptography\"],\"name\":\"windows-sys\",\"req\":\">=0.59.0, <0.62.0\",\"target\":\"cfg(target_env = \\\"msvc\\\")\"}],\"features\":{\"default\":[\"ssl\"],\"force-system-lib-on-osx\":[\"curl-sys/force-system-lib-on-osx\"],\"http2\":[\"curl-sys/http2\"],\"mesalink\":[\"curl-sys/mesalink\"],\"ntlm\":[\"curl-sys/ntlm\"],\"poll_7_68_0\":[\"curl-sys/poll_7_68_0\"],\"protocol-ftp\":[\"curl-sys/protocol-ftp\"],\"rustls\":[\"curl-sys/rustls\"],\"spnego\":[\"curl-sys/spnego\"],\"ssl\":[\"openssl-sys\",\"openssl-probe\",\"curl-sys/ssl\"],\"static-curl\":[\"curl-sys/static-curl\"],\"static-ssl\":[\"curl-sys/static-ssl\"],\"upkeep_7_62_0\":[\"curl-sys/upkeep_7_62_0\"],\"windows-static-ssl\":[\"static-curl\",\"curl-sys/windows-static-ssl\"],\"zlib-ng-compat\":[\"curl-sys/zlib-ng-compat\",\"static-curl\"]}}",
"curve25519-dalek-derive_0.1.1": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.66\"},{\"name\":\"quote\",\"req\":\"^1.0.31\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.27\"}],\"features\":{}}",
"curve25519-dalek_4.1.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2.6\",\"target\":\"cfg(target_arch = \\\"x86_64\\\")\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"curve25519-dalek-derive\",\"req\":\"^0.1\",\"target\":\"cfg(all(not(curve25519_dalek_backend = \\\"fiat\\\"), not(curve25519_dalek_backend = \\\"serial\\\"), target_arch = \\\"x86_64\\\"))\"},{\"default_features\":false,\"name\":\"digest\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"ff\",\"optional\":true,\"req\":\"^0.13\"},{\"default_features\":false,\"name\":\"fiat-crypto\",\"req\":\"^0.2.1\",\"target\":\"cfg(curve25519_dalek_backend = \\\"fiat\\\")\"},{\"default_features\":false,\"name\":\"group\",\"optional\":true,\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6.4\"},{\"default_features\":false,\"features\":[\"getrandom\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.3.0\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"zeroize?/alloc\"],\"default\":[\"alloc\",\"precomputed-tables\",\"zeroize\"],\"group\":[\"dep:group\",\"rand_core\"],\"group-bits\":[\"group\",\"ff/bits\"],\"legacy_compatibility\":[],\"precomputed-tables\":[]}}",
"darling_0.20.11": "{\"dependencies\":[{\"name\":\"darling_core\",\"req\":\"=0.20.11\"},{\"name\":\"darling_macro\",\"req\":\"=0.20.11\"},{\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.86\"},{\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1.0.18\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.9\",\"target\":\"cfg(compiletests)\"},{\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.15\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.89\",\"target\":\"cfg(compiletests)\"}],\"features\":{\"default\":[\"suggestions\"],\"diagnostics\":[\"darling_core/diagnostics\"],\"suggestions\":[\"darling_core/suggestions\"]}}",
@@ -1597,6 +1599,7 @@
"libredox_0.1.14": "{\"dependencies\":[{\"name\":\"bitflags\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"ioslice\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"plain\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"redox_syscall\",\"optional\":true,\"req\":\"^0.7\"}],\"features\":{\"base\":[\"libc\"],\"call\":[\"base\"],\"default\":[\"base\",\"call\",\"std\",\"redox_syscall\",\"protocol\"],\"mkns\":[\"ioslice\"],\"protocol\":[\"plain\",\"bitflags\",\"redox_syscall\"],\"std\":[\"base\"]}}",
"libsqlite3-sys_0.37.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72\"},{\"kind\":\"build\",\"name\":\"cc\",\"optional\":true,\"req\":\"^1.2.27\"},{\"name\":\"openssl-sys\",\"optional\":true,\"req\":\"^0.9.103\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"optional\":true,\"req\":\"^0.3.19\"},{\"kind\":\"build\",\"name\":\"prettyplease\",\"optional\":true,\"req\":\"^0.2.20\"},{\"default_features\":false,\"kind\":\"build\",\"name\":\"quote\",\"optional\":true,\"req\":\"^1.0.36\"},{\"features\":[\"full\",\"extra-traits\",\"visit-mut\"],\"kind\":\"build\",\"name\":\"syn\",\"optional\":true,\"req\":\"^2.0.89\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"optional\":true,\"req\":\"^0.2.15\"}],\"features\":{\"buildtime_bindgen\":[\"bindgen\",\"pkg-config\",\"vcpkg\"],\"bundled\":[\"cc\",\"bundled_bindings\"],\"bundled-sqlcipher\":[\"bundled\"],\"bundled-sqlcipher-vendored-openssl\":[\"bundled-sqlcipher\",\"openssl-sys/vendored\"],\"bundled-windows\":[\"cc\",\"bundled_bindings\"],\"bundled_bindings\":[],\"column_metadata\":[],\"default\":[\"min_sqlite_version_3_34_1\"],\"in_gecko\":[],\"loadable_extension\":[\"prettyplease\",\"quote\",\"syn\"],\"min_sqlite_version_3_34_1\":[\"pkg-config\",\"vcpkg\"],\"preupdate_hook\":[\"buildtime_bindgen\"],\"session\":[\"preupdate_hook\",\"buildtime_bindgen\"],\"sqlcipher\":[],\"unlock_notify\":[],\"wasm32-wasi-vfs\":[],\"with-asan\":[]}}",
"libz-sys_1.1.25": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.98\"},{\"kind\":\"build\",\"name\":\"cmake\",\"optional\":true,\"req\":\"^0.1.50\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.43\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2.11\"}],\"features\":{\"asm\":[],\"default\":[\"libc\",\"stock-zlib\"],\"static\":[],\"stock-zlib\":[],\"zlib-ng\":[\"libc\",\"cmake\"],\"zlib-ng-no-cmake-experimental-community-maintained\":[\"libc\"]}}",
"libz-sys_1.1.29": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.98\"},{\"kind\":\"build\",\"name\":\"cmake\",\"optional\":true,\"req\":\"^0.1.50\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.43\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2.11\"}],\"features\":{\"asm\":[],\"default\":[\"libc\",\"stock-zlib\"],\"static\":[],\"stock-zlib\":[],\"zlib-ng\":[\"libc\",\"cmake\"],\"zlib-ng-no-cmake-experimental-community-maintained\":[\"libc\"]}}",
"line-clipping_0.3.7": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.11\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.26\"}],\"features\":{}}",
"link-section_0.17.2": "{\"dependencies\":[{\"features\":[\"link_section\"],\"name\":\"linktime-proc-macro\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"macrotest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2\"}],\"features\":{\"default\":[\"proc_macro\",\"std\"],\"proc_macro\":[\"dep:linktime-proc-macro\"],\"std\":[]}}",
"linkify_0.11.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.3\"},{\"name\":\"memchr\",\"req\":\"^2.0.1\"}],\"features\":{}}",
+7
View File
@@ -1,3 +1,10 @@
[env]
# curl-sys enables its NTLM sources but does not set the flag required by libcurl 8.21.
CFLAGS_aarch64_pc_windows_gnullvm = "-DCURL_ENABLE_NTLM"
CFLAGS_aarch64_pc_windows_msvc = "-DCURL_ENABLE_NTLM"
CFLAGS_x86_64_pc_windows_gnullvm = "-DCURL_ENABLE_NTLM"
CFLAGS_x86_64_pc_windows_msvc = "-DCURL_ENABLE_NTLM"
[target.'cfg(all(windows, target_env = "msvc"))']
rustflags = ["-C", "link-arg=/STACK:8388608", "-C", "target-feature=+crt-static"]
+46
View File
@@ -3728,6 +3728,7 @@ dependencies = [
"bytes",
"codex-utils-cargo-bin",
"codex-utils-rustls-provider",
"curl",
"futures",
"http 1.4.0",
"http-body 1.0.1",
@@ -3746,13 +3747,16 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"sha2 0.10.9",
"socket2 0.6.3",
"system-configuration",
"tempfile",
"thiserror 2.0.18",
"tokio",
"tokio-rustls",
"tracing",
"tracing-opentelemetry",
"tracing-subscriber",
"url",
"windows-sys 0.61.2",
"zstd",
]
@@ -6123,6 +6127,36 @@ dependencies = [
"cmov",
]
[[package]]
name = "curl"
version = "0.4.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a45ee8994e5307cb4c60cfc1c20bf7263ffb771ddc135c9f768a14bcbc15b09"
dependencies = [
"curl-sys",
"libc",
"openssl-probe 0.1.6",
"openssl-sys",
"schannel",
"socket2 0.6.3",
"windows-sys 0.61.2",
]
[[package]]
name = "curl-sys"
version = "0.4.90+curl-8.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97799a0d220bfb3361e0fe4936966ff8c4b24d65c3f06dfc70d7b680b44e7897"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
"windows-sys 0.61.2",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
@@ -9915,6 +9949,18 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "libz-sys"
version = "1.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "line-clipping"
version = "0.3.7"
+5
View File
@@ -31,7 +31,12 @@ zstd = { workspace = true }
system-configuration = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
curl = { version = "0.4.50", features = ["static-curl", "ntlm"] }
rustls-platform-verifier = "0.7.0"
socket2 = "0.6"
tokio = { workspace = true, features = ["io-util", "net"] }
tokio-rustls = { workspace = true }
url = { workspace = true }
windows-sys = { workspace = true, features = [
"Win32_Foundation",
"Win32_Networking_WinHttp",
+6
View File
@@ -72,8 +72,14 @@ pub use crate::transport::HttpTransport;
pub use crate::transport::ReqwestTransport;
pub use crate::transport::StreamResponse;
#[cfg(windows)]
mod windows_proxy_auth;
#[cfg(windows)]
mod windows_tls;
#[cfg(windows)]
pub use crate::windows_proxy_auth::WindowsProxyTls;
#[cfg(windows)]
pub use crate::windows_proxy_auth::connect_windows_proxy;
#[cfg(windows)]
pub use crate::windows_tls::build_windows_platform_tls_config;
@@ -0,0 +1,265 @@
//! Authenticates a connection to a Windows proxy after a CONNECT 407.
//! Codex owns the connection. Curl reaches it through a local socket used only for
//! authentication. The caller receives the authenticated connection and buffered data.
//! HTTPS proxies that require authentication tokens bound to their TLS connection
//! are not supported because curl does not own that connection.
use std::io;
use std::io::Cursor;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
use std::os::windows::io::IntoRawSocket;
use std::os::windows::io::RawSocket;
use std::sync::Arc;
use std::sync::Mutex;
use curl::easy::Auth;
use curl::easy::Easy2;
use curl::easy::Handler;
use curl::easy::List;
use reqwest::Url;
use rustls::ClientConfig;
use rustls::pki_types::ServerName;
use socket2::Socket;
use tokio::io::AsyncRead;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWrite;
use tokio::io::AsyncWriteExt;
use tokio::io::BufReader;
use tokio::io::Interest;
use tokio::net::TcpListener;
use tokio::net::TcpStream;
use tokio::sync::watch;
use tokio::task::JoinSet;
use tokio_rustls::TlsConnector;
use url::Host;
use crate::build_rustls_client_config_with_custom_ca;
/// Reads and writes a TCP or TLS connection to the proxy.
trait ProxyIo: AsyncRead + AsyncWrite + Unpin + Send {}
impl<T: AsyncRead + AsyncWrite + Unpin + Send> ProxyIo for T {}
struct CurlSocket {
// After CONNECT, this duplicate lets Tokio wait for curl's socket without polling.
// All reads still go through curl so bytes in curl's own buffers are preserved.
readiness: Option<std::net::TcpStream>,
expected_peer: Arc<Mutex<Option<SocketAddr>>>,
cancellation: watch::Sender<()>,
}
impl Handler for CurlSocket {
fn open_socket(&mut self, family: i32, kind: i32, protocol: i32) -> Option<RawSocket> {
let socket = Socket::new(family.into(), kind.into(), Some(protocol.into())).ok()?;
socket
.bind(&SocketAddr::from((Ipv4Addr::LOCALHOST, 0)).into())
.ok()?;
self.readiness = Some(socket.try_clone().ok()?.into());
*self.expected_peer.lock().ok()? = Some(socket.local_addr().ok()?.as_socket()?);
Some(socket.into_raw_socket())
}
fn progress(&mut self, _dltotal: f64, _dlnow: f64, _ultotal: f64, _ulnow: f64) -> bool {
!self.cancellation.is_closed()
}
}
/// TLS configuration for the connection to an HTTPS proxy.
#[derive(Clone)]
pub enum WindowsProxyTls {
/// Use the shared default roots and custom CA configuration.
Default,
/// Reuse the caller's TLS configuration.
Custom(Arc<ClientConfig>),
}
/// Opens a new connection to the selected proxy and authenticates a CONNECT tunnel.
/// Call only after the proxy rejects a CONNECT request with 407.
pub async fn connect_windows_proxy(
proxy_url: &str,
target: &Url,
proxy_tls: WindowsProxyTls,
) -> io::Result<impl AsyncRead + AsyncWrite + Unpin + Send + use<>> {
let proxy = Url::parse(proxy_url).map_err(|_| invalid_proxy())?;
if !matches!(proxy.scheme(), "http" | "https")
|| !proxy.username().is_empty()
|| proxy.password().is_some()
{
return Err(invalid_proxy());
}
let proxy_host = proxy.host_str().ok_or_else(invalid_proxy)?;
let target_host = target.host_str().ok_or_else(invalid_proxy)?;
let target_port = target.port_or_known_default().ok_or_else(invalid_proxy)?;
let listener = Arc::new(TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?);
let local_port = listener.local_addr()?.port();
// Keep the real hostname for Windows' HTTP/<proxy host> identity. Resolve it to our
// local socket for this handle only; Codex connects to the original proxy URL below.
let mut dns = List::new();
dns.append(&format!("{proxy_host}:{local_port}:127.0.0.1"))
.map_err(io::Error::other)?;
let mut auth = Auth::new();
// Curl uses the signed-in user for Negotiate and separately advertised NTLM.
// libcurl 8.22 removes NTLM from Windows Negotiate; check this behavior on upgrades.
auth.gssnegotiate(true).ntlm(true);
let expected_peer = Arc::new(Mutex::new(None));
let (cancellation, _authentication_alive) = watch::channel(());
let mut curl = Easy2::new(CurlSocket {
readiness: None,
expected_peer: Arc::clone(&expected_peer),
cancellation,
});
curl.proxy(&format!("http://{proxy_host}:{local_port}"))
.map_err(io::Error::other)?;
curl.resolve(dns).map_err(io::Error::other)?;
curl.noproxy("").map_err(io::Error::other)?;
curl.url(&format!("http://{target_host}:{target_port}/"))
.map_err(io::Error::other)?;
curl.http_proxy_tunnel(true).map_err(io::Error::other)?;
curl.connect_only(true).map_err(io::Error::other)?;
curl.proxy_username("").map_err(io::Error::other)?;
curl.proxy_password("").map_err(io::Error::other)?;
curl.proxy_auth(&auth).map_err(io::Error::other)?;
curl.progress(true).map_err(io::Error::other)?;
let listener_guard = Arc::clone(&listener);
let mut authentication = tokio::task::spawn_blocking(move || {
// Keep the port reserved until curl stops, even if the caller cancels.
let _listener_guard = listener_guard;
curl.perform().map_err(io::Error::other)?;
Ok::<_, io::Error>(curl)
});
// Curl can reconnect. Keep the proxy connection that completed authentication.
let (authenticated, _) = watch::channel(None);
// Owning the jobs here also closes the real and local sockets if the caller cancels.
let mut connections = JoinSet::new();
let mut curl = loop {
tokio::select! {
result = &mut authentication => break result.map_err(io::Error::other)??,
accepted = listener.accept() => {
let (local, peer) = accepted?;
{
let mut expected = expected_peer
.lock()
.map_err(|_| io::Error::other("proxy socket state is poisoned"))?;
if *expected != Some(peer) {
continue;
}
*expected = None;
}
// Curl has moved to this socket, so earlier attempts cannot authenticate.
connections.shutdown().await;
let proxy = proxy.clone();
let proxy_tls = proxy_tls.clone();
let mut done = authenticated.subscribe();
connections.spawn(async move {
let upstream = connect_to_proxy(&proxy, proxy_tls).await?;
let (mut from_curl, mut to_curl) = local.into_split();
let (from_proxy, mut to_proxy) = tokio::io::split(upstream);
let mut from_proxy = BufReader::new(from_proxy);
tokio::select! {
changed = done.changed() => {
changed.map_err(io::Error::other)?;
let selected = *done.borrow_and_update() == Some(peer.port());
if selected {
// Do not shut down the real writer. EOF on this side lets
// curl return all bytes already sent to its local socket.
to_curl.shutdown().await?;
return Ok(Some((from_proxy, to_proxy)));
}
}
result = async {
tokio::try_join!(
// copy flushes before waiting for more input, so Rustls
// sends CONNECT before curl waits for the proxy response.
tokio::io::copy(&mut from_curl, &mut to_proxy),
async {
tokio::io::copy_buf(&mut from_proxy, &mut to_curl).await?;
to_curl.shutdown().await
},
)
} => { result?; }
}
Ok::<_, io::Error>(None)
});
}
_ = connections.join_next(), if !connections.is_empty() => {}
}
};
authenticated.send_replace(Some(curl.local_port().map_err(io::Error::other)?));
let (from_proxy, to_proxy) = loop {
match connections.join_next().await {
Some(Ok(Ok(Some(halves)))) => break halves,
Some(_) => {}
None => return Err(io::Error::other("authenticated proxy connection ended")),
}
};
let readiness = curl
.get_mut()
.readiness
.take()
.ok_or_else(|| io::Error::other("curl did not open a proxy connection"))?;
let readiness = TcpStream::from_std(readiness)?;
let mut prefix = Vec::new();
let mut buffer = [0; 8192];
loop {
// curl can hold bytes even while the socket isn't readable.
let read = match curl.recv(&mut buffer) {
Ok(read) => read,
Err(error) if error.is_again() => {
readiness
.async_io(Interest::READABLE, || {
curl.recv(&mut buffer).map_err(|error| {
if error.is_again() {
io::ErrorKind::WouldBlock.into()
} else {
io::Error::other(error)
}
})
})
.await?
}
Err(error) => return Err(io::Error::other(error)),
};
if read == 0 {
break;
}
prefix.extend_from_slice(&buffer[..read]);
}
Ok(tokio::io::join(
Cursor::new(prefix).chain(from_proxy),
to_proxy,
))
}
async fn connect_to_proxy(proxy: &Url, proxy_tls: WindowsProxyTls) -> io::Result<Box<dyn ProxyIo>> {
let host = match proxy.host().ok_or_else(invalid_proxy)? {
Host::Domain(domain) => domain.to_owned(),
Host::Ipv4(address) => address.to_string(),
Host::Ipv6(address) => address.to_string(),
};
let port = proxy.port_or_known_default().ok_or_else(invalid_proxy)?;
let tcp = TcpStream::connect((host.as_str(), port)).await?;
tcp.set_nodelay(true)?;
if proxy.scheme() == "https" {
let config = match proxy_tls {
WindowsProxyTls::Default => {
build_rustls_client_config_with_custom_ca().map_err(io::Error::other)?
}
WindowsProxyTls::Custom(config) => config,
};
let server_name = ServerName::try_from(host).map_err(|_| invalid_proxy())?;
Ok(Box::new(
TlsConnector::from(config).connect(server_name, tcp).await?,
))
} else {
Ok(Box::new(tcp))
}
}
fn invalid_proxy() -> io::Error {
io::Error::new(
io::ErrorKind::InvalidInput,
"invalid Windows authentication proxy route",
)
}
@@ -0,0 +1,10 @@
#![cfg(windows)]
#[test]
fn curl_supports_windows_proxy_authentication() {
let version = curl::Version::get();
assert!(
version.feature_ntlm() && version.feature_spnego() && version.feature_sspi(),
"{version:?}"
);
}