From f2ad519a87ca98bb6a673abd54e57fb4cbfeba3c Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 17 Feb 2026 13:49:43 -0800 Subject: [PATCH] feat(network-proxy): add websocket proxy env support (#11784) ## Summary - add managed proxy env wiring for websocket-specific variables (`WS_PROXY`/`WSS_PROXY`, including lowercase) - keep websocket proxy vars aligned with the existing managed HTTP proxy endpoint - add CONNECT regression tests to cover allowlist and denylist decisions (websocket tunnel path) - document websocket proxy usage and CONNECT policy behavior in the network proxy README ## Testing - just fmt - cargo test -p codex-network-proxy - cargo clippy -p codex-network-proxy Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> --- codex-rs/network-proxy/README.md | 5 +++ codex-rs/network-proxy/src/http_proxy.rs | 45 ++++++++++++++++++++++++ codex-rs/network-proxy/src/proxy.rs | 30 ++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/codex-rs/network-proxy/README.md b/codex-rs/network-proxy/README.md index 7ffed9008..925d7147b 100644 --- a/codex-rs/network-proxy/README.md +++ b/codex-rs/network-proxy/README.md @@ -62,6 +62,8 @@ For HTTP(S) traffic: ```bash export HTTP_PROXY="http://127.0.0.1:3128" export HTTPS_PROXY="http://127.0.0.1:3128" +export WS_PROXY="http://127.0.0.1:3128" +export WSS_PROXY="http://127.0.0.1:3128" ``` For SOCKS5 traffic (when `enable_socks5 = true`): @@ -83,6 +85,9 @@ When a request is blocked, the proxy responds with `403` and includes: In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed. HTTPS `CONNECT` and SOCKS5 are blocked because they would bypass method enforcement. +Websocket clients typically tunnel `wss://` through HTTPS `CONNECT`; those CONNECT targets still go +through the same host allowlist/denylist checks. + ## Library API `codex-network-proxy` can be embedded as a library with a thin API: diff --git a/codex-rs/network-proxy/src/http_proxy.rs b/codex-rs/network-proxy/src/http_proxy.rs index eed0ad9da..7be1b81f4 100644 --- a/codex-rs/network-proxy/src/http_proxy.rs +++ b/codex-rs/network-proxy/src/http_proxy.rs @@ -828,6 +828,51 @@ mod tests { ); } + #[tokio::test] + async fn http_connect_accept_allows_allowlisted_host_in_full_mode() { + let policy = NetworkProxySettings { + allowed_domains: vec!["example.com".to_string()], + ..Default::default() + }; + let state = Arc::new(network_proxy_state_for_policy(policy)); + + let mut req = Request::builder() + .method(Method::CONNECT) + .uri("https://example.com:443") + .header("host", "example.com:443") + .body(Body::empty()) + .unwrap(); + req.extensions_mut().insert(state); + + let (response, _request) = http_connect_accept(None, req).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn http_connect_accept_denies_denylisted_host() { + let policy = NetworkProxySettings { + allowed_domains: vec!["**.openai.com".to_string()], + denied_domains: vec!["api.openai.com".to_string()], + ..Default::default() + }; + let state = Arc::new(network_proxy_state_for_policy(policy)); + + let mut req = Request::builder() + .method(Method::CONNECT) + .uri("https://api.openai.com:443") + .header("host", "api.openai.com:443") + .body(Body::empty()) + .unwrap(); + req.extensions_mut().insert(state); + + let response = http_connect_accept(None, req).await.unwrap_err(); + assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!( + response.headers().get("x-proxy-error").unwrap(), + "blocked-by-denylist" + ); + } + #[test] fn request_network_attempt_id_reads_proxy_authorization_header() { let encoded = STANDARD.encode("codex-net-attempt-attempt-1:"); diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 55a0eaa16..2e93fb7a1 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -272,6 +272,8 @@ impl Eq for NetworkProxy {} pub const PROXY_URL_ENV_KEYS: &[&str] = &[ "HTTP_PROXY", "HTTPS_PROXY", + "WS_PROXY", + "WSS_PROXY", "ALL_PROXY", "FTP_PROXY", "YARN_HTTP_PROXY", @@ -290,6 +292,7 @@ pub const ALL_PROXY_ENV_KEYS: &[&str] = &["ALL_PROXY", "all_proxy"]; pub const ALLOW_LOCAL_BINDING_ENV_KEY: &str = "CODEX_NETWORK_ALLOW_LOCAL_BINDING"; const FTP_PROXY_ENV_KEYS: &[&str] = &["FTP_PROXY", "ftp_proxy"]; +const WEBSOCKET_PROXY_ENV_KEYS: &[&str] = &["WS_PROXY", "WSS_PROXY", "ws_proxy", "wss_proxy"]; pub const NO_PROXY_ENV_KEYS: &[&str] = &[ "NO_PROXY", @@ -375,6 +378,9 @@ fn apply_proxy_env_overrides( ], &http_proxy_url, ); + // Some websocket clients look for dedicated WS/WSS proxy environment variables instead of + // HTTP(S)_PROXY. Keep them aligned with the managed HTTP proxy endpoint. + set_env_keys(env, WEBSOCKET_PROXY_ENV_KEYS, &http_proxy_url); // Keep local/private targets direct so local IPC and metadata endpoints avoid the proxy. set_env_keys(env, NO_PROXY_ENV_KEYS, DEFAULT_NO_PROXY_VALUE); @@ -728,6 +734,14 @@ mod tests { assert_eq!(has_proxy_url_env_vars(&env), true); } + #[test] + fn has_proxy_url_env_vars_detects_websocket_proxy_keys() { + let mut env = HashMap::new(); + env.insert("wss_proxy".to_string(), "http://127.0.0.1:3128".to_string()); + + assert_eq!(has_proxy_url_env_vars(&env), true); + } + #[test] fn apply_proxy_env_overrides_sets_common_tool_vars() { let mut env = HashMap::new(); @@ -744,6 +758,14 @@ mod tests { env.get("HTTP_PROXY"), Some(&"http://127.0.0.1:3128".to_string()) ); + assert_eq!( + env.get("WS_PROXY"), + Some(&"http://127.0.0.1:3128".to_string()) + ); + assert_eq!( + env.get("WSS_PROXY"), + Some(&"http://127.0.0.1:3128".to_string()) + ); assert_eq!( env.get("npm_config_proxy"), Some(&"http://127.0.0.1:3128".to_string()) @@ -810,6 +832,14 @@ mod tests { env.get("HTTPS_PROXY"), Some(&"http://codex-net-attempt-attempt-123@127.0.0.1:3128".to_string()) ); + assert_eq!( + env.get("WS_PROXY"), + Some(&"http://codex-net-attempt-attempt-123@127.0.0.1:3128".to_string()) + ); + assert_eq!( + env.get("WSS_PROXY"), + Some(&"http://codex-net-attempt-attempt-123@127.0.0.1:3128".to_string()) + ); assert_eq!( env.get("ALL_PROXY"), Some(&"http://codex-net-attempt-attempt-123@127.0.0.1:3128".to_string())