Add macOS system proxy resolver

This commit is contained in:
canvrno-oai
2026-06-05 16:31:52 -07:00
parent 8a2b6461a5
commit c91497ebef
5 changed files with 429 additions and 9 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -2391,6 +2391,7 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.9",
"system-configuration",
"tempfile",
"thiserror 2.0.18",
"tokio",

View File

@@ -394,6 +394,7 @@ strum_macros = "0.28.0"
supports-color = "3.0.2"
syntect = "5"
sys-locale = "0.3.2"
system-configuration = "0.7"
tar = { version = "=0.4.45", default-features = false }
tempfile = "3.23.0"
test-log = "0.2.19"

View File

@@ -25,8 +25,13 @@ tracing-opentelemetry = { workspace = true }
codex-utils-rustls-provider = { workspace = true }
zstd = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
sha2 = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
system-configuration = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { version = "0.52", features = [
"Win32_Foundation",
"Win32_Networking_WinHttp",

View File

@@ -15,14 +15,16 @@ use std::time::Instant;
use crate::custom_ca::BuildCustomCaTransportError;
use crate::custom_ca::build_reqwest_client_with_custom_ca;
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", target_os = "macos"))]
use sha2::Digest;
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", target_os = "macos"))]
use sha2::Sha256;
use thiserror::Error;
const SYSTEM_PROXY_SUCCESS_CACHE_TTL: Duration = Duration::from_secs(60);
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "windows")]
mod windows;
@@ -294,7 +296,7 @@ fn configure_proxy_for_route(
}
const fn system_proxy_supported() -> bool {
cfg!(target_os = "windows")
cfg!(any(target_os = "windows", target_os = "macos"))
}
fn configure_concrete_proxy(
@@ -373,7 +375,7 @@ fn default_port_for_scheme(scheme: &str) -> Option<u16> {
}
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
#[derive(Debug, Clone, PartialEq, Eq)]
enum SystemProxyDecision {
Direct,
@@ -395,6 +397,15 @@ fn resolve_system_proxy(
decision
}
#[cfg(target_os = "macos")]
fn resolve_platform_system_proxy(
request_url: &str,
origin: &RequestOrigin,
include_auto_detect: bool,
) -> SystemProxyDecision {
macos::resolve(request_url, origin, include_auto_detect)
}
#[cfg(target_os = "windows")]
fn resolve_platform_system_proxy(
request_url: &str,
@@ -404,7 +415,7 @@ fn resolve_platform_system_proxy(
windows::resolve(request_url, origin, include_auto_detect)
}
#[cfg(not(target_os = "windows"))]
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
fn resolve_platform_system_proxy(
_request_url: &str,
_origin: &RequestOrigin,
@@ -463,7 +474,7 @@ fn cache_system_proxy_decision(
}
fn system_proxy_cache_key(request_url: &str, include_auto_detect: bool) -> String {
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", target_os = "macos"))]
{
// Keep URL-specific PAC decisions without retaining the raw routed URL.
let mut hasher = Sha256::new();
@@ -474,7 +485,7 @@ fn system_proxy_cache_key(request_url: &str, include_auto_detect: bool) -> Strin
format!("{:x}", hasher.finalize())
}
#[cfg(not(target_os = "windows"))]
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
format!("{request_url}:auto_detect={include_auto_detect}")
}
@@ -776,7 +787,7 @@ mod tests {
/*include_auto_detect*/ false,
)
);
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", target_os = "macos"))]
assert!(!cache_key.contains(request_url));
}
}

View File

@@ -0,0 +1,402 @@
use std::ffi::c_void;
use std::ptr;
use std::time::Duration;
use std::time::Instant;
use super::RequestOrigin;
use super::RouteFailureClass;
use super::SystemProxyDecision;
use system_configuration::core_foundation::array::CFArray;
use system_configuration::core_foundation::array::CFArrayRef;
use system_configuration::core_foundation::base::CFEqual;
use system_configuration::core_foundation::base::CFGetTypeID;
use system_configuration::core_foundation::base::CFIndex;
use system_configuration::core_foundation::base::CFType;
use system_configuration::core_foundation::base::CFTypeRef;
use system_configuration::core_foundation::base::TCFType;
use system_configuration::core_foundation::base::kCFAllocatorDefault;
use system_configuration::core_foundation::dictionary::CFDictionary;
use system_configuration::core_foundation::dictionary::CFDictionaryRef;
use system_configuration::core_foundation::dictionary::CFMutableDictionary;
use system_configuration::core_foundation::error::CFErrorRef;
use system_configuration::core_foundation::number::CFNumber;
use system_configuration::core_foundation::runloop::CFRunLoop;
use system_configuration::core_foundation::runloop::CFRunLoopSource;
use system_configuration::core_foundation::runloop::CFRunLoopSourceInvalidate;
use system_configuration::core_foundation::runloop::CFRunLoopSourceRef;
use system_configuration::core_foundation::runloop::kCFRunLoopDefaultMode;
use system_configuration::core_foundation::string::CFString;
use system_configuration::core_foundation::string::CFStringRef;
use system_configuration::core_foundation::url::CFURL;
use system_configuration::core_foundation::url::CFURLCreateWithString;
use system_configuration::core_foundation::url::CFURLGetTypeID;
use system_configuration::core_foundation::url::CFURLRef;
use system_configuration::dynamic_store::SCDynamicStoreBuilder;
use system_configuration::sys::schema_definitions::kSCPropNetProxiesProxyAutoDiscoveryEnable;
const PAC_EXECUTION_TIMEOUT: Duration = Duration::from_secs(5);
type ProxyDictionary = CFDictionary<CFString, CFType>;
type ProxyArray = CFArray<ProxyDictionary>;
#[repr(C)]
struct CFStreamClientContext {
version: CFIndex,
info: *mut c_void,
retain: Option<unsafe extern "C" fn(*mut c_void) -> *mut c_void>,
release: Option<unsafe extern "C" fn(*mut c_void)>,
copy_description: Option<unsafe extern "C" fn(*mut c_void) -> CFStringRef>,
}
type CFProxyAutoConfigurationResultCallback =
unsafe extern "C" fn(*mut c_void, CFArrayRef, CFErrorRef);
#[link(name = "CFNetwork", kind = "framework")]
unsafe extern "C" {
static kCFProxyTypeKey: CFStringRef;
static kCFProxyHostNameKey: CFStringRef;
static kCFProxyPortNumberKey: CFStringRef;
static kCFProxyAutoConfigurationURLKey: CFStringRef;
static kCFProxyAutoConfigurationJavaScriptKey: CFStringRef;
static kCFProxyTypeNone: CFStringRef;
static kCFProxyTypeHTTP: CFStringRef;
static kCFProxyTypeHTTPS: CFStringRef;
static kCFProxyTypeSOCKS: CFStringRef;
static kCFProxyTypeAutoConfigurationURL: CFStringRef;
static kCFProxyTypeAutoConfigurationJavaScript: CFStringRef;
fn CFNetworkCopyProxiesForURL(url: CFURLRef, proxy_settings: CFDictionaryRef) -> CFArrayRef;
fn CFNetworkExecuteProxyAutoConfigurationURL(
proxy_auto_config_url: CFURLRef,
target_url: CFURLRef,
callback: CFProxyAutoConfigurationResultCallback,
client_context: *mut CFStreamClientContext,
) -> CFRunLoopSourceRef;
fn CFNetworkExecuteProxyAutoConfigurationScript(
proxy_auto_config_script: CFStringRef,
target_url: CFURLRef,
callback: CFProxyAutoConfigurationResultCallback,
client_context: *mut CFStreamClientContext,
) -> CFRunLoopSourceRef;
}
pub(super) fn resolve(
request_url: &str,
origin: &RequestOrigin,
include_auto_detect: bool,
) -> SystemProxyDecision {
let Some(target_url) = cf_url(request_url) else {
return SystemProxyDecision::Unavailable {
failure: RouteFailureClass::InvalidProxyConfig,
};
};
let Some(settings) = system_proxy_settings(include_auto_detect) else {
return SystemProxyDecision::Unavailable {
failure: RouteFailureClass::ProxyResolutionUnavailable,
};
};
let Some(proxies) = copy_proxies_for_url(&target_url, &settings) else {
return SystemProxyDecision::Unavailable {
failure: RouteFailureClass::ProxyResolutionUnavailable,
};
};
proxy_array_decision(&proxies, &target_url, origin)
}
fn system_proxy_settings(include_auto_detect: bool) -> Option<CFDictionary<CFString, CFType>> {
let store = SCDynamicStoreBuilder::new("Codex").build()?;
let settings = store.get_proxies()?;
if include_auto_detect {
Some(settings)
} else {
Some(settings_without_auto_discovery(&settings))
}
}
fn settings_without_auto_discovery(
settings: &CFDictionary<CFString, CFType>,
) -> CFDictionary<CFString, CFType> {
let mut settings = CFMutableDictionary::from(settings);
let key = unsafe { CFString::wrap_under_get_rule(kSCPropNetProxiesProxyAutoDiscoveryEnable) };
settings.set(key, CFNumber::from(0).into_CFType());
settings.to_immutable()
}
fn copy_proxies_for_url(
target_url: &CFURL,
settings: &CFDictionary<CFString, CFType>,
) -> Option<ProxyArray> {
let proxies = unsafe {
CFNetworkCopyProxiesForURL(
target_url.as_concrete_TypeRef(),
settings.as_concrete_TypeRef(),
)
};
if proxies.is_null() {
None
} else {
Some(unsafe { ProxyArray::wrap_under_create_rule(proxies) })
}
}
fn proxy_array_decision(
proxies: &ProxyArray,
target_url: &CFURL,
origin: &RequestOrigin,
) -> 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 },
ProxyEntryDecision::UnsupportedScheme => saw_unsupported = true,
ProxyEntryDecision::Unavailable => saw_unavailable = true,
}
}
if saw_unsupported {
SystemProxyDecision::Unavailable {
failure: RouteFailureClass::UnsupportedProxyScheme,
}
} else if saw_unavailable {
SystemProxyDecision::Unavailable {
failure: RouteFailureClass::ProxyResolutionUnavailable,
}
} else {
SystemProxyDecision::Direct
}
}
fn proxy_entry_decision(
proxy: &ProxyDictionary,
target_url: &CFURL,
origin: &RequestOrigin,
) -> 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;
}
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeHTTP }) {
return concrete_proxy_entry(proxy, "http");
}
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");
}
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeSOCKS }) {
return ProxyEntryDecision::UnsupportedScheme;
}
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeAutoConfigurationURL }) {
let Some(pac_url) = cf_url_value(proxy, unsafe { kCFProxyAutoConfigurationURLKey }) else {
return ProxyEntryDecision::Unavailable;
};
return pac_decision(execute_pac_url(&pac_url, target_url), target_url, origin);
}
if cf_string_equals(&proxy_type, unsafe {
kCFProxyTypeAutoConfigurationJavaScript
}) {
let Some(script) =
cf_string_value(proxy, unsafe { kCFProxyAutoConfigurationJavaScriptKey })
else {
return ProxyEntryDecision::Unavailable;
};
return pac_decision(execute_pac_script(&script, target_url), target_url, origin);
}
ProxyEntryDecision::Unavailable
}
fn pac_decision(
result: Result<ProxyArray, RouteFailureClass>,
target_url: &CFURL,
origin: &RequestOrigin,
) -> ProxyEntryDecision {
let proxies = match result {
Ok(proxies) => proxies,
Err(RouteFailureClass::UnsupportedProxyScheme) => {
return ProxyEntryDecision::UnsupportedScheme;
}
Err(_) => return ProxyEntryDecision::Unavailable,
};
match proxy_array_decision(&proxies, target_url, origin) {
SystemProxyDecision::Direct => ProxyEntryDecision::Direct,
SystemProxyDecision::Proxy { url } => ProxyEntryDecision::Proxy { url },
SystemProxyDecision::Unavailable {
failure: RouteFailureClass::UnsupportedProxyScheme,
} => ProxyEntryDecision::UnsupportedScheme,
SystemProxyDecision::Unavailable { failure: _ } => ProxyEntryDecision::Unavailable,
}
}
fn execute_pac_url(pac_url: &CFURL, target_url: &CFURL) -> Result<ProxyArray, RouteFailureClass> {
execute_pac(|callback, context| unsafe {
CFNetworkExecuteProxyAutoConfigurationURL(
pac_url.as_concrete_TypeRef(),
target_url.as_concrete_TypeRef(),
callback,
context,
)
})
}
fn execute_pac_script(
script: &CFString,
target_url: &CFURL,
) -> Result<ProxyArray, RouteFailureClass> {
execute_pac(|callback, context| unsafe {
CFNetworkExecuteProxyAutoConfigurationScript(
script.as_concrete_TypeRef(),
target_url.as_concrete_TypeRef(),
callback,
context,
)
})
}
fn execute_pac(
create_source: impl FnOnce(
CFProxyAutoConfigurationResultCallback,
*mut CFStreamClientContext,
) -> CFRunLoopSourceRef,
) -> Result<ProxyArray, RouteFailureClass> {
let mut state = PacRunLoopState { result: None };
let mut context = CFStreamClientContext {
version: 0,
info: (&mut state as *mut PacRunLoopState).cast::<c_void>(),
retain: None,
release: None,
copy_description: None,
};
let source = create_source(pac_result_callback, &mut context);
if source.is_null() {
return Err(RouteFailureClass::ProxyResolutionUnavailable);
}
let source = unsafe { CFRunLoopSource::wrap_under_create_rule(source) };
let run_loop = CFRunLoop::get_current();
let mode = unsafe { kCFRunLoopDefaultMode };
run_loop.add_source(&source, mode);
let started_at = Instant::now();
while state.result.is_none() && started_at.elapsed() < PAC_EXECUTION_TIMEOUT {
CFRunLoop::run_in_mode(mode, Duration::from_millis(50), true);
}
if state.result.is_none() {
unsafe { CFRunLoopSourceInvalidate(source.as_concrete_TypeRef()) };
}
run_loop.remove_source(&source, mode);
state
.result
.unwrap_or(Err(RouteFailureClass::ConnectTimeout))
}
unsafe extern "C" fn pac_result_callback(
client: *mut c_void,
proxies: CFArrayRef,
error: CFErrorRef,
) {
let state = unsafe { &mut *client.cast::<PacRunLoopState>() };
state.result = if !error.is_null() || proxies.is_null() {
Some(Err(RouteFailureClass::ProxyResolutionUnavailable))
} else {
Some(Ok(unsafe { ProxyArray::wrap_under_get_rule(proxies) }))
};
CFRunLoop::get_current().stop();
}
struct PacRunLoopState {
result: Option<Result<ProxyArray, RouteFailureClass>>,
}
fn concrete_proxy_entry(proxy: &ProxyDictionary, proxy_scheme: &str) -> ProxyEntryDecision {
let Some(host) = cf_string_value(proxy, unsafe { kCFProxyHostNameKey })
.map(|host| host.to_string())
.filter(|host| !host.is_empty())
else {
return ProxyEntryDecision::Unavailable;
};
let host = bracket_ipv6_host(&host);
let url = match cf_i32_value(proxy, unsafe { kCFProxyPortNumberKey }) {
Some(port) if port > 0 => format!("{proxy_scheme}://{host}:{port}"),
_ => format!("{proxy_scheme}://{host}"),
};
ProxyEntryDecision::Proxy { url }
}
fn bracket_ipv6_host(host: &str) -> String {
if host.contains(':') && !host.starts_with('[') {
format!("[{host}]")
} else {
host.to_string()
}
}
fn cf_string_value(proxy: &ProxyDictionary, key: CFStringRef) -> Option<CFString> {
proxy
.find(key)
.and_then(|value| value.downcast::<CFString>())
}
fn cf_i32_value(proxy: &ProxyDictionary, key: CFStringRef) -> Option<i32> {
proxy
.find(key)
.and_then(|value| value.downcast::<CFNumber>())
.and_then(|value| value.to_i32())
}
fn cf_url_value(proxy: &ProxyDictionary, key: CFStringRef) -> Option<CFURL> {
proxy.find(key).and_then(|value| {
if unsafe { CFGetTypeID(value.as_CFTypeRef()) == CFURLGetTypeID() } {
Some(unsafe { CFURL::wrap_under_get_rule(value.as_CFTypeRef() as CFURLRef) })
} else {
value
.downcast::<CFString>()
.and_then(|value| cf_url(value.to_string().as_str()))
}
})
}
fn cf_string_equals(value: &CFString, expected: CFStringRef) -> bool {
unsafe { CFEqual(value.as_CFTypeRef(), expected as CFTypeRef) != 0 }
}
fn cf_url(value: &str) -> Option<CFURL> {
let value = CFString::new(value);
let url = unsafe {
CFURLCreateWithString(
kCFAllocatorDefault,
value.as_concrete_TypeRef(),
ptr::null(),
)
};
if url.is_null() {
None
} else {
Some(unsafe { CFURL::wrap_under_create_rule(url) })
}
}
enum ProxyEntryDecision {
Direct,
Proxy { url: String },
UnsupportedScheme,
Unavailable,
}