diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index d2b8ee75b9..e1a1e1985a 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -396,45 +396,6 @@ dependencies = [ "term", ] -[[package]] -name = "asn1-rs" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" -dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", - "displaydoc", - "nom 7.1.3", - "num-traits", - "rusticata-macros", - "thiserror 2.0.17", - "time", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", - "synstructure", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "assert-json-diff" version = "2.0.2" @@ -1718,12 +1679,10 @@ dependencies = [ "rama-http", "rama-http-backend", "rama-net", - "rama-socks5", "rama-tcp", "rama-tls-boring", "rama-unix", "rama-utils", - "rcgen", "serde", "serde_json", "time", @@ -2652,20 +2611,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "der-parser" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" -dependencies = [ - "asn1-rs", - "displaydoc", - "nom 7.1.3", - "num-bigint", - "num-traits", - "rusticata-macros", -] - [[package]] name = "deranged" version = "0.5.4" @@ -5128,15 +5073,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "oid-registry" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" -dependencies = [ - "asn1-rs", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -5419,16 +5355,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "pem" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64", - "serde_core", -] - [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -6139,21 +6065,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "rama-socks5" -version = "0.3.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5468b263516daaf258de32542c1974b7cbe962363ad913dcb669f5d46db0ef3e" -dependencies = [ - "byteorder", - "rama-core", - "rama-net", - "rama-tcp", - "rama-udp", - "rama-utils", - "tokio", -] - [[package]] name = "rama-tcp" version = "0.3.0-alpha.4" @@ -6192,18 +6103,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "rama-udp" -version = "0.3.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ed05e0ecac73e084e92a3a8b1fbf16fdae8958c506f0f0eada180a2d99eef4" -dependencies = [ - "rama-core", - "rama-net", - "tokio", - "tokio-util", -] - [[package]] name = "rama-unix" version = "0.3.0-alpha.4" @@ -6351,20 +6250,6 @@ dependencies = [ "ratatui", ] -[[package]] -name = "rcgen" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec0a99f2de91c3cddc84b37e7db80e4d96b743e05607f647eb236fc0455907f" -dependencies = [ - "pem", - "ring", - "rustls-pki-types", - "time", - "x509-parser", - "yasna", -] - [[package]] name = "redox_syscall" version = "0.5.15" @@ -6595,15 +6480,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom 7.1.3", -] - [[package]] name = "rustix" version = "0.38.44" @@ -9652,24 +9528,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" -[[package]] -name = "x509-parser" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3e137310115a65136898d2079f003ce33331a6c4b0d51f1531d1be082b6425" -dependencies = [ - "asn1-rs", - "data-encoding", - "der-parser", - "lazy_static", - "nom 7.1.3", - "oid-registry", - "ring", - "rusticata-macros", - "thiserror 2.0.17", - "time", -] - [[package]] name = "xdg-home" version = "1.3.0" @@ -9686,15 +9544,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" -[[package]] -name = "yasna" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" -dependencies = [ - "time", -] - [[package]] name = "yoke" version = "0.8.0" diff --git a/codex-rs/network-proxy/Cargo.toml b/codex-rs/network-proxy/Cargo.toml index 7c0c790d41..d4762e3fcc 100644 --- a/codex-rs/network-proxy/Cargo.toml +++ b/codex-rs/network-proxy/Cargo.toml @@ -22,7 +22,6 @@ clap = { workspace = true, features = ["derive"] } codex-app-server-protocol = { workspace = true } codex-core = { workspace = true } globset = { workspace = true } -rcgen-rama = { package = "rcgen", version = "0.14", default-features = false, features = ["pem", "x509-parser", "ring"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } time = { workspace = true } @@ -33,7 +32,6 @@ rama-core = { version = "=0.3.0-alpha.4" } rama-http = { version = "=0.3.0-alpha.4" } rama-http-backend = { version = "=0.3.0-alpha.4", features = ["tls"] } rama-net = { version = "=0.3.0-alpha.4", features = ["http", "tls"] } -rama-socks5 = { version = "=0.3.0-alpha.4" } rama-tcp = { version = "=0.3.0-alpha.4", features = ["http"] } rama-tls-boring = { version = "=0.3.0-alpha.4", features = ["http"] } rama-utils = { version = "=0.3.0-alpha.4" } diff --git a/codex-rs/network-proxy/README.md b/codex-rs/network-proxy/README.md index a3a783b889..1d19a92a68 100644 --- a/codex-rs/network-proxy/README.md +++ b/codex-rs/network-proxy/README.md @@ -3,7 +3,6 @@ `codex-network-proxy` is Codex's local network policy enforcement proxy. It runs: - an HTTP proxy (default `127.0.0.1:3128`) -- a SOCKS5 proxy (default `127.0.0.1:8081`) - an admin HTTP API (default `127.0.0.1:8080`) It enforces an allow/deny policy and a "limited" mode intended for read-only network access. @@ -44,42 +43,15 @@ allow_local_binding = false # macOS-only: allows proxying to a unix socket when request includes `x-unix-socket: /path`. allow_unix_sockets = ["/tmp/example.sock"] - -[network_proxy.mitm] -# Enables CONNECT MITM for limited-mode HTTPS. If disabled, CONNECT is blocked in limited mode. -enabled = true - -# When true, logs request/response body sizes (up to max_body_bytes). -inspect = false -max_body_bytes = 4096 - -# These are resolved relative to $CODEX_HOME when relative. -ca_cert_path = "network_proxy/mitm/ca.pem" -ca_key_path = "network_proxy/mitm/ca.key" ``` -### 2) Initialize MITM directories (optional) - -This ensures the MITM directory exists (and is a good smoke test that the binary runs): - -```bash -cargo run -p codex-network-proxy -- init -``` - -### 3) Run the proxy +### 2) Run the proxy ```bash cargo run -p codex-network-proxy -- ``` -Optional flags: - -```bash -# Enable SOCKS5 UDP associate support (off by default). -cargo run -p codex-network-proxy -- --enable-socks5-udp -``` - -### 4) Point a client at it +### 3) Point a client at it For HTTP(S) traffic: @@ -88,13 +60,7 @@ export HTTP_PROXY="http://127.0.0.1:3128" export HTTPS_PROXY="http://127.0.0.1:3128" ``` -For SOCKS5 traffic: - -```bash -export ALL_PROXY="socks5://127.0.0.1:8081" -``` - -### 5) Understand blocks / debugging +### 4) Understand blocks / debugging When a request is blocked, the proxy responds with `403` and includes: @@ -102,13 +68,10 @@ When a request is blocked, the proxy responds with `403` and includes: - `blocked-by-allowlist` - `blocked-by-denylist` - `blocked-by-method-policy` - - `blocked-by-mitm-required` - `blocked-by-policy` -In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed. In addition, HTTPS `CONNECT` -requires MITM to be enabled to allow read-only HTTPS; otherwise the proxy blocks CONNECT with -reason `mitm_required`. In "full" mode, CONNECT is always a transparent tunnel even if MITM -is enabled. +In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed for plain HTTP. HTTPS `CONNECT` +remains a transparent tunnel, so limited-mode method enforcement does not apply to HTTPS. ## Library API @@ -119,7 +82,6 @@ use codex_network_proxy::{NetworkProxy, NetworkDecision, NetworkPolicyRequest}; let proxy = NetworkProxy::builder() .http_addr("127.0.0.1:8080".parse()?) - .socks_addr("127.0.0.1:1080".parse()?) .admin_addr("127.0.0.1:9000".parse()?) .policy_decider(|request: NetworkPolicyRequest| async move { // Example: auto-allow when exec policy already approved a command prefix. @@ -175,7 +137,7 @@ curl -sS -X POST http://127.0.0.1:8080/reload - Unix socket proxying via the `x-unix-socket` header is **macOS-only**; other platforms will reject unix socket requests. -- MITM TLS termination uses BoringSSL via Rama's `rama-tls-boring`; building the proxy requires a +- HTTPS tunneling uses BoringSSL via Rama's `rama-tls-boring`; building the proxy requires a native toolchain and CMake on macOS/Linux/Windows. ## Security notes (important) @@ -191,7 +153,7 @@ what it can reasonably guarantee. allowlisted (best-effort DNS lookup). - Limited mode enforcement: - only `GET`, `HEAD`, and `OPTIONS` are allowed - - HTTPS `CONNECT` requires MITM to be enabled, otherwise CONNECT is blocked (to avoid “tunnel hides method” bypass). + - HTTPS `CONNECT` remains a tunnel; limited-mode method enforcement does not apply to HTTPS - Listener safety defaults: - the admin API is unauthenticated; non-loopback binds are clamped unless explicitly enabled via `dangerously_allow_non_loopback_admin` @@ -200,10 +162,6 @@ what it can reasonably guarantee. - when unix socket proxying is enabled, both listeners are forced to loopback to avoid turning the proxy into a remote bridge into local daemons. - `enabled` is enforced at runtime; when false the proxy no-ops and does not bind listeners. -- MITM CA key handling: - - the CA key file is created with restrictive permissions (`0600`) and written atomically using - create-new + fsync + rename, to avoid partial writes or transiently-permissive modes. - Limitations: - DNS rebinding is hard to fully prevent without pinning the resolved IP(s) all the way down to the diff --git a/codex-rs/network-proxy/src/config.rs b/codex-rs/network-proxy/src/config.rs index 011a68a3db..b2d5b450a7 100644 --- a/codex-rs/network-proxy/src/config.rs +++ b/codex-rs/network-proxy/src/config.rs @@ -2,7 +2,6 @@ use serde::Deserialize; use serde::Serialize; use std::net::IpAddr; use std::net::SocketAddr; -use std::path::PathBuf; use tracing::warn; #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -29,8 +28,6 @@ pub struct NetworkProxyConfig { pub mode: NetworkMode, #[serde(default)] pub policy: NetworkPolicy, - #[serde(default)] - pub mitm: MitmConfig, } impl Default for NetworkProxyConfig { @@ -44,7 +41,6 @@ impl Default for NetworkProxyConfig { dangerously_allow_non_loopback_admin: false, mode: NetworkMode::default(), policy: NetworkPolicy::default(), - mitm: MitmConfig::default(), } } } @@ -69,32 +65,6 @@ pub enum NetworkMode { Full, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MitmConfig { - #[serde(default)] - pub enabled: bool, - #[serde(default)] - pub inspect: bool, - #[serde(default = "default_mitm_max_body_bytes")] - pub max_body_bytes: usize, - #[serde(default = "default_ca_cert_path")] - pub ca_cert_path: PathBuf, - #[serde(default = "default_ca_key_path")] - pub ca_key_path: PathBuf, -} - -impl Default for MitmConfig { - fn default() -> Self { - Self { - enabled: false, - inspect: false, - max_body_bytes: default_mitm_max_body_bytes(), - ca_cert_path: default_ca_cert_path(), - ca_key_path: default_ca_key_path(), - } - } -} - fn default_proxy_url() -> String { "http://127.0.0.1:3128".to_string() } @@ -103,18 +73,6 @@ fn default_admin_url() -> String { "http://127.0.0.1:8080".to_string() } -fn default_ca_cert_path() -> PathBuf { - PathBuf::from("network_proxy/mitm/ca.pem") -} - -fn default_ca_key_path() -> PathBuf { - PathBuf::from("network_proxy/mitm/ca.key") -} - -fn default_mitm_max_body_bytes() -> usize { - 4096 -} - fn clamp_non_loopback(addr: SocketAddr, allow_non_loopback: bool, name: &str) -> SocketAddr { if addr.ip().is_loopback() { return addr; @@ -173,7 +131,6 @@ pub(crate) fn clamp_bind_addrs( pub struct RuntimeConfig { pub http_addr: SocketAddr, - pub socks_addr: SocketAddr, pub admin_addr: SocketAddr, } @@ -181,11 +138,9 @@ pub fn resolve_runtime(cfg: &Config) -> RuntimeConfig { let http_addr = resolve_addr(&cfg.network_proxy.proxy_url, 3128); let admin_addr = resolve_addr(&cfg.network_proxy.admin_url, 8080); let (http_addr, admin_addr) = clamp_bind_addrs(http_addr, admin_addr, &cfg.network_proxy); - let socks_addr = SocketAddr::from(([127, 0, 0, 1], 8081)); RuntimeConfig { http_addr, - socks_addr, admin_addr, } } diff --git a/codex-rs/network-proxy/src/http_proxy.rs b/codex-rs/network-proxy/src/http_proxy.rs index 47e20950a3..1dd65d88cc 100644 --- a/codex-rs/network-proxy/src/http_proxy.rs +++ b/codex-rs/network-proxy/src/http_proxy.rs @@ -1,5 +1,4 @@ use crate::config::NetworkMode; -use crate::mitm; use crate::network_policy::NetworkDecision; use crate::network_policy::NetworkPolicyDecider; use crate::network_policy::NetworkPolicyRequest; @@ -196,39 +195,8 @@ async fn http_connect_accept( } }; - let mitm_state = match app_state.mitm_state().await { - Ok(state) => state, - Err(err) => { - error!("failed to load MITM state: {err}"); - return Err(text_response(StatusCode::INTERNAL_SERVER_ERROR, "error")); - } - }; - - if mode == NetworkMode::Limited && mitm_state.is_none() { - // Limited mode is designed to be read-only. Without MITM, a CONNECT tunnel would hide the - // inner HTTP method/headers from the proxy, effectively bypassing method policy. - let _ = app_state - .record_blocked(BlockedRequest::new( - host.clone(), - "mitm_required".to_string(), - client.clone(), - Some("CONNECT".to_string()), - Some(NetworkMode::Limited), - "http-connect".to_string(), - )) - .await; - let client = client.as_deref().unwrap_or_default(); - warn!( - "CONNECT blocked; MITM required for read-only HTTPS in limited mode (client={client}, host={host}, mode=limited, allowed_methods=GET, HEAD, OPTIONS)" - ); - return Err(blocked_text("mitm_required")); - } - req.extensions_mut().insert(ProxyTarget(authority)); req.extensions_mut().insert(mode); - if let Some(mitm_state) = mitm_state { - req.extensions_mut().insert(mitm_state); - } Ok(( Response::builder() @@ -240,12 +208,6 @@ async fn http_connect_accept( } async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> { - let mode = upgraded - .extensions() - .get::() - .copied() - .unwrap_or(NetworkMode::Full); - let Some(target) = upgraded .extensions() .get::() @@ -254,21 +216,7 @@ async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> { warn!("CONNECT missing proxy target"); return Ok(()); }; - let host = normalize_host(&target.host.to_string()); - - if mode == NetworkMode::Limited - && upgraded - .extensions() - .get::>() - .is_some() - { - let port = target.port; - info!("CONNECT MITM enabled (host={host}, port={port}, mode={mode:?})"); - if let Err(err) = mitm::mitm_tunnel(upgraded).await { - warn!("MITM tunnel error: {err}"); - } - return Ok(()); - } + let _host = normalize_host(&target.host.to_string()); let allow_upstream_proxy = match upgraded.extensions().get::>().cloned() { Some(state) => match state.allow_upstream_proxy().await { diff --git a/codex-rs/network-proxy/src/init.rs b/codex-rs/network-proxy/src/init.rs deleted file mode 100644 index 236e617819..0000000000 --- a/codex-rs/network-proxy/src/init.rs +++ /dev/null @@ -1,17 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use codex_core::config::find_codex_home; -use std::fs; - -pub fn run_init() -> Result<()> { - let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?; - let root = codex_home.join("network_proxy"); - let mitm_dir = root.join("mitm"); - - fs::create_dir_all(&root).with_context(|| format!("failed to create {}", root.display()))?; - fs::create_dir_all(&mitm_dir) - .with_context(|| format!("failed to create {}", mitm_dir.display()))?; - - println!("ensured {}", mitm_dir.display()); - Ok(()) -} diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index 00b9c98c70..b044787a8b 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -1,14 +1,11 @@ mod admin; mod config; mod http_proxy; -mod init; -mod mitm; mod network_policy; mod policy; mod proxy; mod responses; mod runtime; -mod socks5; mod state; mod upstream; @@ -18,18 +15,11 @@ pub use network_policy::NetworkPolicyDecider; pub use network_policy::NetworkPolicyRequest; pub use network_policy::NetworkProtocol; pub use proxy::Args; -pub use proxy::Command; pub use proxy::NetworkProxy; pub use proxy::NetworkProxyBuilder; pub use proxy::NetworkProxyHandle; -pub use proxy::run_init; pub async fn run_main(args: Args) -> Result<()> { - if let Some(Command::Init) = args.command { - run_init()?; - return Ok(()); - } - let proxy = NetworkProxy::from_cli_args(args).await?; proxy.run().await?.wait().await } diff --git a/codex-rs/network-proxy/src/main.rs b/codex-rs/network-proxy/src/main.rs index f76a3ac2b4..f298f068e8 100644 --- a/codex-rs/network-proxy/src/main.rs +++ b/codex-rs/network-proxy/src/main.rs @@ -1,20 +1,13 @@ use anyhow::Result; use clap::Parser; use codex_network_proxy::Args; -use codex_network_proxy::Command; use codex_network_proxy::NetworkProxy; -use codex_network_proxy::run_init; #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt::init(); let args = Args::parse(); - if let Some(Command::Init) = args.command { - run_init()?; - return Ok(()); - } - let proxy = NetworkProxy::from_cli_args(args).await?; proxy.run().await?.wait().await } diff --git a/codex-rs/network-proxy/src/mitm.rs b/codex-rs/network-proxy/src/mitm.rs deleted file mode 100644 index d99e48dfd6..0000000000 --- a/codex-rs/network-proxy/src/mitm.rs +++ /dev/null @@ -1,622 +0,0 @@ -use crate::config::MitmConfig; -use crate::config::NetworkMode; -use crate::policy::method_allowed; -use crate::policy::normalize_host; -use crate::responses::blocked_text_response; -use crate::state::AppState; -use crate::state::BlockedRequest; -use crate::upstream::UpstreamClient; -use anyhow::Context as _; -use anyhow::Result; -use anyhow::anyhow; -use rama_core::Layer; -use rama_core::Service; -use rama_core::bytes::Bytes; -use rama_core::error::BoxError; -use rama_core::extensions::ExtensionsRef; -use rama_core::futures::stream::Stream; -use rama_core::rt::Executor; -use rama_core::service::service_fn; -use rama_http::Body; -use rama_http::BodyDataStream; -use rama_http::HeaderValue; -use rama_http::Request; -use rama_http::Response; -use rama_http::StatusCode; -use rama_http::Uri; -use rama_http::header::HOST; -use rama_http::layer::remove_header::RemoveRequestHeaderLayer; -use rama_http::layer::remove_header::RemoveResponseHeaderLayer; -use rama_http_backend::server::HttpServer; -use rama_http_backend::server::layer::upgrade::Upgraded; -use rama_net::proxy::ProxyTarget; -use rama_net::stream::SocketInfo; -use rama_net::tls::ApplicationProtocol; -use rama_net::tls::DataEncoding; -use rama_net::tls::server::ServerAuth; -use rama_net::tls::server::ServerAuthData; -use rama_net::tls::server::ServerConfig; -use rama_tls_boring::server::TlsAcceptorData; -use rama_tls_boring::server::TlsAcceptorLayer; -use rama_utils::str::NonEmptyStr; -use std::fs; -use std::fs::File; -use std::fs::OpenOptions; -use std::io::Write; -use std::net::IpAddr; -use std::pin::Pin; -use std::sync::Arc; -use std::task::Context as TaskContext; -use std::task::Poll; -use std::time::SystemTime; -use std::time::UNIX_EPOCH; -use tracing::info; -use tracing::warn; - -use rcgen_rama::BasicConstraints; -use rcgen_rama::CertificateParams; -use rcgen_rama::DistinguishedName; -use rcgen_rama::DnType; -use rcgen_rama::ExtendedKeyUsagePurpose; -use rcgen_rama::IsCa; -use rcgen_rama::Issuer; -use rcgen_rama::KeyPair; -use rcgen_rama::KeyUsagePurpose; -use rcgen_rama::SanType; - -pub struct MitmState { - issuer: Issuer<'static, KeyPair>, - upstream: UpstreamClient, - inspect: bool, - max_body_bytes: usize, -} - -impl std::fmt::Debug for MitmState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // Avoid dumping internal state (CA material, connectors, etc.) to logs. - f.debug_struct("MitmState") - .field("inspect", &self.inspect) - .field("max_body_bytes", &self.max_body_bytes) - .finish_non_exhaustive() - } -} - -impl MitmState { - pub fn new(cfg: &MitmConfig, allow_upstream_proxy: bool) -> Result { - // MITM exists to make limited-mode HTTPS enforceable: once CONNECT is established, plain - // proxying would lose visibility into the inner HTTP request. We generate/load a local CA - // and issue per-host leaf certs so we can terminate TLS and apply policy. - let (ca_cert_pem, ca_key_pem) = load_or_create_ca(cfg)?; - let ca_key = KeyPair::from_pem(&ca_key_pem).context("failed to parse CA key")?; - let issuer: Issuer<'static, KeyPair> = - Issuer::from_ca_cert_pem(&ca_cert_pem, ca_key).context("failed to parse CA cert")?; - - let upstream = if allow_upstream_proxy { - UpstreamClient::from_env_proxy() - } else { - UpstreamClient::direct() - }; - - Ok(Self { - issuer, - upstream, - inspect: cfg.inspect, - max_body_bytes: cfg.max_body_bytes, - }) - } - - fn tls_acceptor_data_for_host(&self, host: &str) -> Result { - let (cert_pem, key_pem) = issue_host_certificate_pem(host, &self.issuer)?; - let cert_chain = DataEncoding::Pem( - NonEmptyStr::try_from(cert_pem.as_str()).context("failed to encode host cert PEM")?, - ); - let private_key = DataEncoding::Pem( - NonEmptyStr::try_from(key_pem.as_str()).context("failed to encode host key PEM")?, - ); - let auth = ServerAuthData { - private_key, - cert_chain, - ocsp: None, - }; - - let mut server_config = ServerConfig::new(ServerAuth::Single(auth)); - server_config.application_layer_protocol_negotiation = Some(vec![ - ApplicationProtocol::HTTP_2, - ApplicationProtocol::HTTP_11, - ]); - - TlsAcceptorData::try_from(server_config).context("failed to build boring acceptor config") - } - - pub fn inspect_enabled(&self) -> bool { - self.inspect - } - - pub fn max_body_bytes(&self) -> usize { - self.max_body_bytes - } -} - -pub async fn mitm_tunnel(upgraded: Upgraded) -> Result<()> { - let state = upgraded - .extensions() - .get::>() - .cloned() - .context("missing MITM state")?; - let target = upgraded - .extensions() - .get::() - .context("missing proxy target")? - .0 - .clone(); - let host = normalize_host(&target.host.to_string()); - let acceptor_data = state.tls_acceptor_data_for_host(&host)?; - - let executor = upgraded - .extensions() - .get::() - .cloned() - .unwrap_or_default(); - - let http_service = HttpServer::auto(executor).service( - ( - RemoveResponseHeaderLayer::hop_by_hop(), - RemoveRequestHeaderLayer::hop_by_hop(), - ) - .into_layer(service_fn(handle_mitm_request)), - ); - - let https_service = TlsAcceptorLayer::new(acceptor_data) - .with_store_client_hello(true) - .into_layer(http_service); - - https_service - .serve(upgraded) - .await - .map_err(|err| anyhow!("MITM serve error: {err}"))?; - Ok(()) -} - -async fn handle_mitm_request(req: Request) -> Result { - let response = match forward_request(req).await { - Ok(resp) => resp, - Err(err) => { - warn!("MITM upstream request failed: {err}"); - text_response(StatusCode::BAD_GATEWAY, "mitm upstream error") - } - }; - Ok(response) -} - -async fn forward_request(req: Request) -> Result { - let target = req - .extensions() - .get::() - .context("missing proxy target")? - .0 - .clone(); - - let target_host = normalize_host(&target.host.to_string()); - let target_port = target.port; - let mode = req - .extensions() - .get::() - .copied() - .unwrap_or(NetworkMode::Full); - let mitm = req - .extensions() - .get::>() - .cloned() - .context("missing MITM state")?; - let app_state = req - .extensions() - .get::>() - .cloned() - .context("missing app state")?; - - if req.method().as_str() == "CONNECT" { - return Ok(text_response( - StatusCode::METHOD_NOT_ALLOWED, - "CONNECT not supported inside MITM", - )); - } - - let method = req.method().as_str().to_string(); - let path = path_and_query(req.uri()); - let client = req - .extensions() - .get::() - .map(|info| info.peer_addr().to_string()); - - if let Some(request_host) = extract_request_host(&req) { - let normalized = normalize_host(&request_host); - if !normalized.is_empty() && normalized != target_host { - warn!("MITM host mismatch (target={target_host}, request_host={normalized})"); - return Ok(text_response(StatusCode::BAD_REQUEST, "host mismatch")); - } - } - - if !method_allowed(mode, method.as_str()) { - let _ = app_state - .record_blocked(BlockedRequest::new( - target_host.clone(), - "method_not_allowed".to_string(), - client.clone(), - Some(method.clone()), - Some(NetworkMode::Limited), - "https".to_string(), - )) - .await; - warn!( - "MITM blocked by method policy (host={target_host}, method={method}, path={path}, mode={mode:?}, allowed_methods=GET, HEAD, OPTIONS)" - ); - return Ok(blocked_text("method_not_allowed")); - } - - let (mut parts, body) = req.into_parts(); - let authority = authority_header_value(&target_host, target_port); - parts.uri = build_https_uri(&authority, &path)?; - parts - .headers - .insert(HOST, HeaderValue::from_str(&authority)?); - - let inspect = mitm.inspect_enabled(); - let max_body_bytes = mitm.max_body_bytes(); - let body = if inspect { - inspect_body( - body, - max_body_bytes, - RequestLogContext { - host: authority.clone(), - method: method.clone(), - path: path.clone(), - }, - ) - } else { - body - }; - - let upstream_req = Request::from_parts(parts, body); - let upstream_resp = mitm.upstream.serve(upstream_req).await?; - respond_with_inspection( - upstream_resp, - inspect, - max_body_bytes, - &method, - &path, - &authority, - ) -} - -fn respond_with_inspection( - resp: Response, - inspect: bool, - max_body_bytes: usize, - method: &str, - path: &str, - authority: &str, -) -> Result { - if !inspect { - return Ok(resp); - } - - let (parts, body) = resp.into_parts(); - let body = inspect_body( - body, - max_body_bytes, - ResponseLogContext { - host: authority.to_string(), - method: method.to_string(), - path: path.to_string(), - status: parts.status, - }, - ); - Ok(Response::from_parts(parts, body)) -} - -fn inspect_body( - body: Body, - max_body_bytes: usize, - ctx: T, -) -> Body { - Body::from_stream(InspectStream { - inner: Box::pin(body.into_data_stream()), - ctx: Some(Box::new(ctx)), - len: 0, - max_body_bytes, - }) -} - -struct InspectStream { - inner: Pin>, - ctx: Option>, - len: usize, - max_body_bytes: usize, -} - -impl Stream for InspectStream { - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - let this = self.get_mut(); - match this.inner.as_mut().poll_next(cx) { - Poll::Ready(Some(Ok(bytes))) => { - this.len = this.len.saturating_add(bytes.len()); - Poll::Ready(Some(Ok(bytes))) - } - Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))), - Poll::Ready(None) => { - if let Some(ctx) = this.ctx.take() { - ctx.log(this.len, this.len > this.max_body_bytes); - } - Poll::Ready(None) - } - Poll::Pending => Poll::Pending, - } - } -} - -struct RequestLogContext { - host: String, - method: String, - path: String, -} - -struct ResponseLogContext { - host: String, - method: String, - path: String, - status: StatusCode, -} - -trait BodyLoggable { - fn log(self, len: usize, truncated: bool); -} - -impl BodyLoggable for RequestLogContext { - fn log(self, len: usize, truncated: bool) { - let host = self.host; - let method = self.method; - let path = self.path; - info!( - "MITM inspected request body (host={host}, method={method}, path={path}, body_len={len}, truncated={truncated})" - ); - } -} - -impl BodyLoggable for ResponseLogContext { - fn log(self, len: usize, truncated: bool) { - let host = self.host; - let method = self.method; - let path = self.path; - let status = self.status; - info!( - "MITM inspected response body (host={host}, method={method}, path={path}, status={status}, body_len={len}, truncated={truncated})" - ); - } -} - -fn extract_request_host(req: &Request) -> Option { - req.headers() - .get(HOST) - .and_then(|v| v.to_str().ok()) - .map(ToString::to_string) - .or_else(|| req.uri().authority().map(|a| a.as_str().to_string())) -} - -fn authority_header_value(host: &str, port: u16) -> String { - // Host header / URI authority formatting. - if host.contains(':') { - if port == 443 { - format!("[{host}]") - } else { - format!("[{host}]:{port}") - } - } else if port == 443 { - host.to_string() - } else { - format!("{host}:{port}") - } -} - -fn build_https_uri(authority: &str, path: &str) -> Result { - let target = format!("https://{authority}{path}"); - Ok(target.parse()?) -} - -fn path_and_query(uri: &Uri) -> String { - uri.path_and_query() - .map(rama_http::uri::PathAndQuery::as_str) - .unwrap_or("/") - .to_string() -} - -fn issue_host_certificate_pem( - host: &str, - issuer: &Issuer<'_, KeyPair>, -) -> Result<(String, String)> { - let mut params = if let Ok(ip) = host.parse::() { - let mut params = CertificateParams::new(Vec::new()) - .map_err(|err| anyhow!("failed to create cert params: {err}"))?; - params.subject_alt_names.push(SanType::IpAddress(ip)); - params - } else { - CertificateParams::new(vec![host.to_string()]) - .map_err(|err| anyhow!("failed to create cert params: {err}"))? - }; - - params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth]; - params.key_usages = vec![ - KeyUsagePurpose::DigitalSignature, - KeyUsagePurpose::KeyEncipherment, - ]; - - let key_pair = KeyPair::generate_for(&rcgen_rama::PKCS_ECDSA_P256_SHA256) - .map_err(|err| anyhow!("failed to generate host key pair: {err}"))?; - let cert = params - .signed_by(&key_pair, issuer) - .map_err(|err| anyhow!("failed to sign host cert: {err}"))?; - - Ok((cert.pem(), key_pair.serialize_pem())) -} - -fn load_or_create_ca(cfg: &MitmConfig) -> Result<(String, String)> { - let cert_path = &cfg.ca_cert_path; - let key_path = &cfg.ca_key_path; - - if cert_path.exists() || key_path.exists() { - if !cert_path.exists() || !key_path.exists() { - return Err(anyhow!("both ca_cert_path and ca_key_path must exist")); - } - let cert_pem = fs::read_to_string(cert_path) - .with_context(|| format!("failed to read CA cert {}", cert_path.display()))?; - let key_pem = fs::read_to_string(key_path) - .with_context(|| format!("failed to read CA key {}", key_path.display()))?; - return Ok((cert_pem, key_pem)); - } - - if let Some(parent) = cert_path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("failed to create {}", parent.display()))?; - } - if let Some(parent) = key_path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("failed to create {}", parent.display()))?; - } - - let (cert_pem, key_pem) = generate_ca()?; - // The CA key is a high-value secret. Create it atomically with restrictive permissions. - // The cert can be world-readable, but we still write it atomically to avoid partial writes. - // - // We intentionally use create-new semantics: if a key already exists, we should not overwrite - // it silently (that would invalidate previously-trusted cert chains). - write_atomic_create_new(key_path, key_pem.as_bytes(), 0o600) - .with_context(|| format!("failed to persist CA key {}", key_path.display()))?; - if let Err(err) = write_atomic_create_new(cert_path, cert_pem.as_bytes(), 0o644) - .with_context(|| format!("failed to persist CA cert {}", cert_path.display())) - { - // Avoid leaving a partially-created CA around (cert missing) if the second write fails. - let _ = fs::remove_file(key_path); - return Err(err); - } - let cert_path = cert_path.display(); - let key_path = key_path.display(); - info!("generated MITM CA (cert_path={cert_path}, key_path={key_path})"); - Ok((cert_pem, key_pem)) -} - -fn generate_ca() -> Result<(String, String)> { - let mut params = CertificateParams::default(); - params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); - params.key_usages = vec![ - KeyUsagePurpose::KeyCertSign, - KeyUsagePurpose::DigitalSignature, - KeyUsagePurpose::KeyEncipherment, - ]; - let mut dn = DistinguishedName::new(); - dn.push(DnType::CommonName, "network_proxy MITM CA"); - params.distinguished_name = dn; - - let key_pair = KeyPair::generate_for(&rcgen_rama::PKCS_ECDSA_P256_SHA256) - .map_err(|err| anyhow!("failed to generate CA key pair: {err}"))?; - let cert = params - .self_signed(&key_pair) - .map_err(|err| anyhow!("failed to generate CA cert: {err}"))?; - Ok((cert.pem(), key_pair.serialize_pem())) -} - -fn write_atomic_create_new(path: &std::path::Path, contents: &[u8], mode: u32) -> Result<()> { - let parent = path - .parent() - .ok_or_else(|| anyhow!("missing parent directory"))?; - - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_nanos(); - let pid = std::process::id(); - let file_name = path.file_name().unwrap_or_default().to_string_lossy(); - let tmp_path = parent.join(format!(".{file_name}.tmp.{pid}.{nanos}")); - - let mut file = open_create_new_with_mode(&tmp_path, mode)?; - file.write_all(contents) - .with_context(|| format!("failed to write {}", tmp_path.display()))?; - file.sync_all() - .with_context(|| format!("failed to fsync {}", tmp_path.display()))?; - drop(file); - - // Create the final file using "create-new" semantics (no overwrite). `rename` on Unix can - // overwrite existing files, so prefer a hard-link, which fails if the destination exists. - match fs::hard_link(&tmp_path, path) { - Ok(()) => { - fs::remove_file(&tmp_path) - .with_context(|| format!("failed to remove {}", tmp_path.display()))?; - } - Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { - let _ = fs::remove_file(&tmp_path); - return Err(anyhow!( - "refusing to overwrite existing file {}", - path.display() - )); - } - Err(_) => { - // Best-effort fallback for environments where hard links are not supported. - // This is still subject to a TOCTOU race, but the typical case is a private per-user - // config directory, where other users cannot create files anyway. - if path.exists() { - let _ = fs::remove_file(&tmp_path); - return Err(anyhow!( - "refusing to overwrite existing file {}", - path.display() - )); - } - fs::rename(&tmp_path, path).with_context(|| { - format!( - "failed to rename {} -> {}", - tmp_path.display(), - path.display() - ) - })?; - } - } - - // Best-effort durability: ensure the directory entry is persisted too. - let dir = File::open(parent).with_context(|| format!("failed to open {}", parent.display()))?; - dir.sync_all() - .with_context(|| format!("failed to fsync {}", parent.display()))?; - - Ok(()) -} - -#[cfg(unix)] -fn open_create_new_with_mode(path: &std::path::Path, mode: u32) -> Result { - use std::os::unix::fs::OpenOptionsExt; - - OpenOptions::new() - .write(true) - .create_new(true) - .mode(mode) - .open(path) - .with_context(|| format!("failed to create {}", path.display())) -} - -#[cfg(not(unix))] -fn open_create_new_with_mode(path: &std::path::Path, _mode: u32) -> Result { - OpenOptions::new() - .write(true) - .create_new(true) - .open(path) - .with_context(|| format!("failed to create {}", path.display())) -} - -fn blocked_text(reason: &str) -> Response { - blocked_text_response(reason) -} - -fn text_response(status: StatusCode, body: &str) -> Response { - Response::builder() - .status(status) - .header("content-type", "text/plain") - .body(Body::from(body.to_string())) - .unwrap_or_else(|_| Response::new(Body::from(body.to_string()))) -} diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 68ea93ccbf..984481ccc7 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -1,13 +1,10 @@ use crate::admin; use crate::config; use crate::http_proxy; -use crate::init; use crate::network_policy::NetworkPolicyDecider; -use crate::socks5; use crate::state::AppState; use anyhow::Result; use clap::Parser; -use clap::Subcommand; use std::net::SocketAddr; use std::sync::Arc; use tokio::task::JoinHandle; @@ -15,28 +12,14 @@ use tracing::warn; #[derive(Debug, Clone, Parser)] #[command(name = "codex-network-proxy", about = "Codex network sandbox proxy")] -pub struct Args { - #[command(subcommand)] - pub command: Option, - /// Enable SOCKS5 UDP associate support (default: disabled). - #[arg(long, default_value_t = false)] - pub enable_socks5_udp: bool, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum Command { - /// Initialize the Codex network proxy directories (e.g. MITM cert paths). - Init, -} +pub struct Args {} #[derive(Clone, Default)] pub struct NetworkProxyBuilder { state: Option>, http_addr: Option, - socks_addr: Option, admin_addr: Option, policy_decider: Option>, - enable_socks5_udp: bool, } impl NetworkProxyBuilder { @@ -52,12 +35,6 @@ impl NetworkProxyBuilder { self } - #[must_use] - pub fn socks_addr(mut self, addr: SocketAddr) -> Self { - self.socks_addr = Some(addr); - self - } - #[must_use] pub fn admin_addr(mut self, addr: SocketAddr) -> Self { self.admin_addr = Some(addr); @@ -79,12 +56,6 @@ impl NetworkProxyBuilder { self } - #[must_use] - pub fn enable_socks5_udp(mut self, enabled: bool) -> Self { - self.enable_socks5_udp = enabled; - self - } - pub async fn build(self) -> Result { let state = match self.state { Some(state) => state, @@ -102,10 +73,8 @@ impl NetworkProxyBuilder { Ok(NetworkProxy { state, http_addr, - socks_addr: self.socks_addr.unwrap_or(runtime.socks_addr), admin_addr, policy_decider: self.policy_decider, - enable_socks5_udp: self.enable_socks5_udp, }) } } @@ -114,10 +83,8 @@ impl NetworkProxyBuilder { pub struct NetworkProxy { state: Arc, http_addr: SocketAddr, - socks_addr: SocketAddr, admin_addr: SocketAddr, policy_decider: Option>, - enable_socks5_udp: bool, } impl NetworkProxy { @@ -126,9 +93,8 @@ impl NetworkProxy { NetworkProxyBuilder::default() } - pub async fn from_cli_args(args: Args) -> Result { - let mut builder = Self::builder(); - builder = builder.enable_socks5_udp(args.enable_socks5_udp); + pub async fn from_cli_args(_args: Args) -> Result { + let builder = Self::builder(); builder.build().await } @@ -148,17 +114,10 @@ impl NetworkProxy { self.http_addr, self.policy_decider.clone(), )); - let socks_task = tokio::spawn(socks5::run_socks5( - self.state.clone(), - self.socks_addr, - self.policy_decider.clone(), - self.enable_socks5_udp, - )); let admin_task = tokio::spawn(admin::run_admin_api(self.state.clone(), self.admin_addr)); Ok(NetworkProxyHandle { http_task, - socks_task, admin_task, }) } @@ -166,7 +125,6 @@ impl NetworkProxy { pub struct NetworkProxyHandle { http_task: JoinHandle>, - socks_task: JoinHandle>, admin_task: JoinHandle>, } @@ -174,29 +132,21 @@ impl NetworkProxyHandle { fn noop() -> Self { Self { http_task: tokio::spawn(async { Ok(()) }), - socks_task: tokio::spawn(async { Ok(()) }), admin_task: tokio::spawn(async { Ok(()) }), } } pub async fn wait(self) -> Result<()> { self.http_task.await??; - self.socks_task.await??; self.admin_task.await??; Ok(()) } pub async fn shutdown(self) -> Result<()> { self.http_task.abort(); - self.socks_task.abort(); self.admin_task.abort(); let _ = self.http_task.await; - let _ = self.socks_task.await; let _ = self.admin_task.await; Ok(()) } } - -pub fn run_init() -> Result<()> { - init::run_init() -} diff --git a/codex-rs/network-proxy/src/responses.rs b/codex-rs/network-proxy/src/responses.rs index 6633621912..011362070e 100644 --- a/codex-rs/network-proxy/src/responses.rs +++ b/codex-rs/network-proxy/src/responses.rs @@ -28,7 +28,6 @@ pub fn blocked_header_value(reason: &str) -> &'static str { "not_allowed" | "not_allowed_local" => "blocked-by-allowlist", "denied" => "blocked-by-denylist", "method_not_allowed" => "blocked-by-method-policy", - "mitm_required" => "blocked-by-mitm-required", _ => "blocked-by-policy", } } @@ -39,7 +38,6 @@ pub fn blocked_message(reason: &str) -> &'static str { "not_allowed_local" => "Codex blocked this request: local/private addresses not allowed.", "denied" => "Codex blocked this request: domain denied by policy.", "method_not_allowed" => "Codex blocked this request: method not allowed in limited mode.", - "mitm_required" => "Codex blocked this request: MITM required for limited HTTPS.", _ => "Codex blocked this request by network policy.", } } diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index 56da739abb..21bbf99ba0 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -1,6 +1,5 @@ use crate::config::Config; use crate::config::NetworkMode; -use crate::mitm::MitmState; use crate::policy::is_loopback_host; use crate::policy::is_non_public_ip; use crate::policy::method_allowed; @@ -68,7 +67,6 @@ pub(crate) struct ConfigState { pub(crate) mtime: Option, pub(crate) allow_set: GlobSet, pub(crate) deny_set: GlobSet, - pub(crate) mitm: Option>, pub(crate) constraints: NetworkProxyConstraints, pub(crate) cfg_path: PathBuf, pub(crate) blocked: VecDeque, @@ -283,12 +281,6 @@ impl AppState { Ok(()) } - pub async fn mitm_state(&self) -> Result>> { - self.reload_if_needed().await?; - let guard = self.state.read().await; - Ok(guard.mitm.clone()) - } - async fn reload_if_needed(&self) -> Result<()> { let needs_reload = { let guard = self.state.read().await; @@ -415,7 +407,6 @@ pub(crate) fn app_state_for_policy(policy: crate::config::NetworkPolicy) -> AppS mtime: None, allow_set, deny_set, - mitm: None, constraints: NetworkProxyConstraints::default(), cfg_path: PathBuf::from("/nonexistent/config.toml"), blocked: VecDeque::new(), diff --git a/codex-rs/network-proxy/src/socks5.rs b/codex-rs/network-proxy/src/socks5.rs deleted file mode 100644 index 6d04187fbc..0000000000 --- a/codex-rs/network-proxy/src/socks5.rs +++ /dev/null @@ -1,302 +0,0 @@ -use crate::config::NetworkMode; -use crate::network_policy::NetworkDecision; -use crate::network_policy::NetworkPolicyDecider; -use crate::network_policy::NetworkPolicyRequest; -use crate::network_policy::NetworkProtocol; -use crate::network_policy::evaluate_host_policy; -use crate::policy::normalize_host; -use crate::state::AppState; -use crate::state::BlockedRequest; -use anyhow::Context as _; -use anyhow::Result; -use rama_core::Layer; -use rama_core::Service; -use rama_core::extensions::ExtensionsRef; -use rama_core::layer::AddInputExtensionLayer; -use rama_core::service::service_fn; -use rama_net::stream::SocketInfo; -use rama_socks5::Socks5Acceptor; -use rama_socks5::server::DefaultConnector; -use rama_socks5::server::DefaultUdpRelay; -use rama_socks5::server::udp::RelayRequest; -use rama_socks5::server::udp::RelayResponse; -use rama_tcp::client::Request as TcpRequest; -use rama_tcp::client::service::TcpConnector; -use rama_tcp::server::TcpListener; -use std::io; -use std::net::SocketAddr; -use std::sync::Arc; -use tracing::error; -use tracing::info; -use tracing::warn; - -pub async fn run_socks5( - state: Arc, - addr: SocketAddr, - policy_decider: Option>, - enable_socks5_udp: bool, -) -> Result<()> { - let listener = TcpListener::build() - .bind(addr) - .await - // See `http_proxy.rs` for details on why we wrap `BoxError` before converting to anyhow. - .map_err(rama_core::error::OpaqueError::from) - .map_err(anyhow::Error::from) - .with_context(|| format!("bind SOCKS5 proxy: {addr}"))?; - - info!("SOCKS5 proxy listening on {addr}"); - - match state.network_mode().await { - Ok(NetworkMode::Limited) => { - info!("SOCKS5 is blocked in limited mode; set mode=\"full\" to allow SOCKS5"); - } - Ok(NetworkMode::Full) => {} - Err(err) => { - warn!("failed to read network mode: {err}"); - } - } - - let tcp_connector = TcpConnector::default(); - let policy_tcp_connector = service_fn({ - let policy_decider = policy_decider.clone(); - move |req: TcpRequest| { - let tcp_connector = tcp_connector.clone(); - let policy_decider = policy_decider.clone(); - async move { - let app_state = req - .extensions() - .get::>() - .cloned() - .ok_or_else(|| io::Error::other("missing state"))?; - - let host = normalize_host(&req.authority.host.to_string()); - let port = req.authority.port; - let client = req - .extensions() - .get::() - .map(|info| info.peer_addr().to_string()); - match app_state.enabled().await { - Ok(true) => {} - Ok(false) => { - let _ = app_state - .record_blocked(BlockedRequest::new( - host.clone(), - "proxy_disabled".to_string(), - client.clone(), - None, - None, - "socks5".to_string(), - )) - .await; - let client = client.as_deref().unwrap_or_default(); - warn!("SOCKS blocked; proxy disabled (client={client}, host={host})"); - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "proxy disabled", - ) - .into()); - } - Err(err) => { - error!("failed to read enabled state: {err}"); - return Err(io::Error::other("proxy error").into()); - } - } - match app_state.network_mode().await { - Ok(NetworkMode::Limited) => { - let _ = app_state - .record_blocked(BlockedRequest::new( - host.clone(), - "method_not_allowed".to_string(), - client.clone(), - None, - Some(NetworkMode::Limited), - "socks5".to_string(), - )) - .await; - let client = client.as_deref().unwrap_or_default(); - warn!( - "SOCKS blocked by method policy (client={client}, host={host}, mode=limited, allowed_methods=GET, HEAD, OPTIONS)" - ); - return Err( - io::Error::new(io::ErrorKind::PermissionDenied, "blocked").into() - ); - } - Ok(NetworkMode::Full) => {} - Err(err) => { - error!("failed to evaluate method policy: {err}"); - return Err(io::Error::other("proxy error").into()); - } - } - - let request = NetworkPolicyRequest::new( - NetworkProtocol::Socks5Tcp, - host.clone(), - port, - client.clone(), - None, - None, - None, - ); - - match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await { - Ok(NetworkDecision::Deny { reason }) => { - let _ = app_state - .record_blocked(BlockedRequest::new( - host.clone(), - reason.clone(), - client.clone(), - None, - None, - "socks5".to_string(), - )) - .await; - let client = client.as_deref().unwrap_or_default(); - warn!("SOCKS blocked (client={client}, host={host}, reason={reason})"); - return Err( - io::Error::new(io::ErrorKind::PermissionDenied, "blocked").into() - ); - } - Ok(NetworkDecision::Allow) => { - let client = client.as_deref().unwrap_or_default(); - info!("SOCKS allowed (client={client}, host={host}, port={port})"); - } - Err(err) => { - error!("failed to evaluate host: {err}"); - return Err(io::Error::other("proxy error").into()); - } - } - - tcp_connector.serve(req).await - } - } - }); - - let socks_connector = DefaultConnector::default().with_connector(policy_tcp_connector); - let base = Socks5Acceptor::new().with_connector(socks_connector); - - if enable_socks5_udp { - let udp_state = state.clone(); - let udp_decider = policy_decider.clone(); - let udp_relay = DefaultUdpRelay::default().with_async_inspector(service_fn( - move |request: RelayRequest| { - let udp_state = udp_state.clone(); - let udp_decider = udp_decider.clone(); - async move { - let RelayRequest { - server_address, - payload, - extensions, - .. - } = request; - - let host = normalize_host(&server_address.ip_addr.to_string()); - let port = server_address.port; - let client = extensions - .get::() - .map(|info| info.peer_addr().to_string()); - match udp_state.enabled().await { - Ok(true) => {} - Ok(false) => { - let _ = udp_state - .record_blocked(BlockedRequest::new( - host.clone(), - "proxy_disabled".to_string(), - client.clone(), - None, - None, - "socks5-udp".to_string(), - )) - .await; - let client = client.as_deref().unwrap_or_default(); - warn!( - "SOCKS UDP blocked; proxy disabled (client={client}, host={host})" - ); - return Ok(RelayResponse { - maybe_payload: None, - extensions, - }); - } - Err(err) => { - error!("failed to read enabled state: {err}"); - return Err(io::Error::other("proxy error")); - } - } - - match udp_state.network_mode().await { - Ok(NetworkMode::Limited) => { - let _ = udp_state - .record_blocked(BlockedRequest::new( - host.clone(), - "method_not_allowed".to_string(), - client.clone(), - None, - Some(NetworkMode::Limited), - "socks5-udp".to_string(), - )) - .await; - return Ok(RelayResponse { - maybe_payload: None, - extensions, - }); - } - Ok(NetworkMode::Full) => {} - Err(err) => { - error!("failed to evaluate method policy: {err}"); - return Err(io::Error::other("proxy error")); - } - } - - let request = NetworkPolicyRequest::new( - NetworkProtocol::Socks5Udp, - host.clone(), - port, - client.clone(), - None, - None, - None, - ); - - match evaluate_host_policy(&udp_state, udp_decider.as_ref(), &request).await { - Ok(NetworkDecision::Deny { reason }) => { - let _ = udp_state - .record_blocked(BlockedRequest::new( - host.clone(), - reason.clone(), - client.clone(), - None, - None, - "socks5-udp".to_string(), - )) - .await; - let client = client.as_deref().unwrap_or_default(); - warn!( - "SOCKS UDP blocked (client={client}, host={host}, reason={reason})" - ); - Ok(RelayResponse { - maybe_payload: None, - extensions, - }) - } - Ok(NetworkDecision::Allow) => Ok(RelayResponse { - maybe_payload: Some(payload), - extensions, - }), - Err(err) => { - error!("failed to evaluate UDP host: {err}"); - Err(io::Error::other("proxy error")) - } - } - } - }, - )); - let socks_acceptor = base.with_udp_associator(udp_relay); - listener - .serve(AddInputExtensionLayer::new(state).into_layer(socks_acceptor)) - .await; - } else { - listener - .serve(AddInputExtensionLayer::new(state).into_layer(base)) - .await; - } - Ok(()) -} diff --git a/codex-rs/network-proxy/src/state.rs b/codex-rs/network-proxy/src/state.rs index b3808607a0..442abc4315 100644 --- a/codex-rs/network-proxy/src/state.rs +++ b/codex-rs/network-proxy/src/state.rs @@ -1,7 +1,5 @@ use crate::config::Config; -use crate::config::MitmConfig; use crate::config::NetworkMode; -use crate::mitm::MitmState; use crate::policy::DomainPattern; use crate::policy::compile_globset; use crate::runtime::ConfigState; @@ -15,8 +13,6 @@ use codex_core::config::ConstraintError; use codex_core::config_loader::RequirementSource; use serde::Deserialize; use std::collections::HashSet; -use std::path::Path; -use std::sync::Arc; pub use crate::runtime::AppState; pub use crate::runtime::BlockedRequest; @@ -36,7 +32,7 @@ pub(crate) async fn build_config_state() -> Result { // Deserialize from the merged effective config, rather than parsing config.toml ourselves. // This avoids a second parser/merger implementation (and the drift that comes with it). let merged_toml = codex_cfg.config_layer_stack.effective_config(); - let mut config: Config = merged_toml + let config: Config = merged_toml .try_into() .context("failed to deserialize network proxy config")?; @@ -44,52 +40,20 @@ pub(crate) async fn build_config_state() -> Result { // trusted/managed layers (e.g., MDM). Enforce this before building runtime state. let constraints = enforce_trusted_constraints(&codex_cfg.config_layer_stack, &config)?; - // Permit relative MITM paths for ergonomics; resolve them relative to CODEX_HOME so the - // proxy can be configured from multiple config locations without changing cert paths. - resolve_mitm_paths(&mut config, &codex_cfg.codex_home); let mtime = cfg_path.metadata().and_then(|m| m.modified()).ok(); let deny_set = compile_globset(&config.network_proxy.policy.denied_domains)?; let allow_set = compile_globset(&config.network_proxy.policy.allowed_domains)?; - let mitm = if config.network_proxy.mitm.enabled { - build_mitm_state( - &config.network_proxy.mitm, - config.network_proxy.allow_upstream_proxy, - )? - } else { - None - }; Ok(ConfigState { config, mtime, allow_set, deny_set, - mitm, constraints, cfg_path, blocked: std::collections::VecDeque::new(), }) } -fn resolve_mitm_paths(config: &mut Config, codex_home: &Path) { - let base = codex_home; - if config.network_proxy.mitm.ca_cert_path.is_relative() { - config.network_proxy.mitm.ca_cert_path = base.join(&config.network_proxy.mitm.ca_cert_path); - } - if config.network_proxy.mitm.ca_key_path.is_relative() { - config.network_proxy.mitm.ca_key_path = base.join(&config.network_proxy.mitm.ca_key_path); - } -} - -fn build_mitm_state( - config: &MitmConfig, - allow_upstream_proxy: bool, -) -> Result>> { - Ok(Some(Arc::new(MitmState::new( - config, - allow_upstream_proxy, - )?))) -} - #[derive(Debug, Default, Deserialize)] struct PartialConfig { #[serde(default)]