From 877b76bb9d95969bac667d1facd2b37831588d15 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 27 Jan 2026 10:09:39 -0800 Subject: [PATCH] feat(network-proxy): add a SOCKS5 proxy with policy enforcement (#9803) ### Summary - Adds an optional SOCKS5 listener via `rama-socks5` - SOCKS5 is disabled by default and gated by config - Reuses existing policy enforcement and blocked-request recording - Blocks SOCKS5 in limited mode to prevent method-policy bypass - Applies bind clamping to the SOCKS5 listener ### Config New/used fields under `network_proxy`: - `enable_socks5` - `socks_url` - `enable_socks5_udp` ### Scope - Changes limited to `codex-rs/network-proxy` (+ `codex-rs/Cargo.lock`) ### Testing ```bash cd codex-rs just fmt cargo test -p codex-network-proxy --offline --- codex-rs/Cargo.lock | 28 +++ codex-rs/network-proxy/Cargo.toml | 1 + codex-rs/network-proxy/README.md | 17 +- codex-rs/network-proxy/src/config.rs | 50 ++++- codex-rs/network-proxy/src/lib.rs | 1 + codex-rs/network-proxy/src/proxy.rs | 56 ++++- codex-rs/network-proxy/src/socks5.rs | 318 +++++++++++++++++++++++++++ 7 files changed, 452 insertions(+), 19 deletions(-) create mode 100644 codex-rs/network-proxy/src/socks5.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 75d72d360..43916d1e1 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1683,6 +1683,7 @@ dependencies = [ "rama-http", "rama-http-backend", "rama-net", + "rama-socks5", "rama-tcp", "rama-tls-boring", "rama-unix", @@ -5959,6 +5960,21 @@ 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" @@ -5997,6 +6013,18 @@ 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" diff --git a/codex-rs/network-proxy/Cargo.toml b/codex-rs/network-proxy/Cargo.toml index 2d303b1c9..959837d4e 100644 --- a/codex-rs/network-proxy/Cargo.toml +++ b/codex-rs/network-proxy/Cargo.toml @@ -34,6 +34,7 @@ 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"] } diff --git a/codex-rs/network-proxy/README.md b/codex-rs/network-proxy/README.md index 1d19a92a6..3d8c20307 100644 --- a/codex-rs/network-proxy/README.md +++ b/codex-rs/network-proxy/README.md @@ -3,6 +3,7 @@ `codex-network-proxy` is Codex's local network policy enforcement proxy. It runs: - an HTTP proxy (default `127.0.0.1:3128`) +- an optional SOCKS5 proxy (default `127.0.0.1:8081`, disabled by default) - 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. @@ -20,6 +21,10 @@ Example config: enabled = true proxy_url = "http://127.0.0.1:3128" admin_url = "http://127.0.0.1:8080" +# Optional SOCKS5 listener (disabled by default). +enable_socks5 = false +socks_url = "http://127.0.0.1:8081" +enable_socks5_udp = false # When `enabled` is false, the proxy no-ops and does not bind listeners. # When true, respect HTTP(S)_PROXY/ALL_PROXY for upstream requests (HTTP(S) proxies only), # including CONNECT tunnels in full mode. @@ -28,7 +33,7 @@ allow_upstream_proxy = false # If you want to expose these listeners beyond localhost, you must opt in explicitly. dangerously_allow_non_loopback_proxy = false dangerously_allow_non_loopback_admin = false -mode = "limited" # or "full" +mode = "full" # default when unset; use "limited" for read-only mode [network_proxy.policy] # Hosts must match the allowlist (unless denied). @@ -60,6 +65,12 @@ export HTTP_PROXY="http://127.0.0.1:3128" export HTTPS_PROXY="http://127.0.0.1:3128" ``` +For SOCKS5 traffic (when `enable_socks5 = true`): + +```bash +export ALL_PROXY="socks5h://127.0.0.1:8081" +``` + ### 4) Understand blocks / debugging When a request is blocked, the proxy responds with `403` and includes: @@ -70,8 +81,8 @@ When a request is blocked, the proxy responds with `403` and includes: - `blocked-by-method-policy` - `blocked-by-policy` -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. +In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed. HTTPS `CONNECT` and SOCKS5 are +blocked because they would bypass method enforcement. ## Library API diff --git a/codex-rs/network-proxy/src/config.rs b/codex-rs/network-proxy/src/config.rs index 7bd6957ac..ae4a45017 100644 --- a/codex-rs/network-proxy/src/config.rs +++ b/codex-rs/network-proxy/src/config.rs @@ -23,6 +23,12 @@ pub struct NetworkProxySettings { #[serde(default = "default_admin_url")] pub admin_url: String, #[serde(default)] + pub enable_socks5: bool, + #[serde(default = "default_socks_url")] + pub socks_url: String, + #[serde(default)] + pub enable_socks5_udp: bool, + #[serde(default)] pub allow_upstream_proxy: bool, #[serde(default)] pub dangerously_allow_non_loopback_proxy: bool, @@ -40,6 +46,9 @@ impl Default for NetworkProxySettings { enabled: false, proxy_url: default_proxy_url(), admin_url: default_admin_url(), + enable_socks5: false, + socks_url: default_socks_url(), + enable_socks5_udp: false, allow_upstream_proxy: false, dangerously_allow_non_loopback_proxy: false, dangerously_allow_non_loopback_admin: false, @@ -90,6 +99,10 @@ fn default_admin_url() -> String { "http://127.0.0.1:8080".to_string() } +fn default_socks_url() -> String { + "http://127.0.0.1:8081".to_string() +} + /// Clamp non-loopback bind addresses to loopback unless explicitly allowed. fn clamp_non_loopback(addr: SocketAddr, allow_non_loopback: bool, name: &str) -> SocketAddr { if addr.ip().is_loopback() { @@ -110,21 +123,27 @@ fn clamp_non_loopback(addr: SocketAddr, allow_non_loopback: bool, name: &str) -> pub(crate) fn clamp_bind_addrs( http_addr: SocketAddr, + socks_addr: SocketAddr, admin_addr: SocketAddr, cfg: &NetworkProxySettings, -) -> (SocketAddr, SocketAddr) { +) -> (SocketAddr, SocketAddr, SocketAddr) { let http_addr = clamp_non_loopback( http_addr, cfg.dangerously_allow_non_loopback_proxy, "HTTP proxy", ); + let socks_addr = clamp_non_loopback( + socks_addr, + cfg.dangerously_allow_non_loopback_proxy, + "SOCKS5 proxy", + ); let admin_addr = clamp_non_loopback( admin_addr, cfg.dangerously_allow_non_loopback_admin, "admin API", ); if cfg.policy.allow_unix_sockets.is_empty() { - return (http_addr, admin_addr); + return (http_addr, socks_addr, admin_addr); } // `x-unix-socket` is intentionally a local escape hatch. If the proxy (or admin API) is @@ -136,6 +155,11 @@ pub(crate) fn clamp_bind_addrs( "unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_proxy and clamping HTTP proxy to loopback" ); } + if cfg.dangerously_allow_non_loopback_proxy && !socks_addr.ip().is_loopback() { + warn!( + "unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_proxy and clamping SOCKS5 proxy to loopback" + ); + } if cfg.dangerously_allow_non_loopback_admin && !admin_addr.ip().is_loopback() { warn!( "unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_admin and clamping admin API to loopback" @@ -143,12 +167,14 @@ pub(crate) fn clamp_bind_addrs( } ( SocketAddr::from(([127, 0, 0, 1], http_addr.port())), + SocketAddr::from(([127, 0, 0, 1], socks_addr.port())), SocketAddr::from(([127, 0, 0, 1], admin_addr.port())), ) } pub struct RuntimeConfig { pub http_addr: SocketAddr, + pub socks_addr: SocketAddr, pub admin_addr: SocketAddr, } @@ -159,16 +185,24 @@ pub fn resolve_runtime(cfg: &NetworkProxyConfig) -> Result { cfg.network_proxy.proxy_url ) })?; + let socks_addr = resolve_addr(&cfg.network_proxy.socks_url, 8081).with_context(|| { + format!( + "invalid network_proxy.socks_url: {}", + cfg.network_proxy.socks_url + ) + })?; let admin_addr = resolve_addr(&cfg.network_proxy.admin_url, 8080).with_context(|| { format!( "invalid network_proxy.admin_url: {}", cfg.network_proxy.admin_url ) })?; - let (http_addr, admin_addr) = clamp_bind_addrs(http_addr, admin_addr, &cfg.network_proxy); + let (http_addr, socks_addr, admin_addr) = + clamp_bind_addrs(http_addr, socks_addr, admin_addr, &cfg.network_proxy); Ok(RuntimeConfig { http_addr, + socks_addr, admin_addr, }) } @@ -403,11 +437,14 @@ mod tests { ..Default::default() }; let http_addr = "0.0.0.0:3128".parse::().unwrap(); + let socks_addr = "0.0.0.0:8081".parse::().unwrap(); let admin_addr = "0.0.0.0:8080".parse::().unwrap(); - let (http_addr, admin_addr) = clamp_bind_addrs(http_addr, admin_addr, &cfg); + let (http_addr, socks_addr, admin_addr) = + clamp_bind_addrs(http_addr, socks_addr, admin_addr, &cfg); assert_eq!(http_addr, "0.0.0.0:3128".parse::().unwrap()); + assert_eq!(socks_addr, "0.0.0.0:8081".parse::().unwrap()); assert_eq!(admin_addr, "0.0.0.0:8080".parse::().unwrap()); } @@ -423,11 +460,14 @@ mod tests { ..Default::default() }; let http_addr = "0.0.0.0:3128".parse::().unwrap(); + let socks_addr = "0.0.0.0:8081".parse::().unwrap(); let admin_addr = "0.0.0.0:8080".parse::().unwrap(); - let (http_addr, admin_addr) = clamp_bind_addrs(http_addr, admin_addr, &cfg); + let (http_addr, socks_addr, admin_addr) = + clamp_bind_addrs(http_addr, socks_addr, admin_addr, &cfg); assert_eq!(http_addr, "127.0.0.1:3128".parse::().unwrap()); + assert_eq!(socks_addr, "127.0.0.1:8081".parse::().unwrap()); assert_eq!(admin_addr, "127.0.0.1:8080".parse::().unwrap()); } } diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index 71e4ec735..f4bcb8c74 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -9,6 +9,7 @@ mod proxy; mod reasons; mod responses; mod runtime; +mod socks5; mod state; mod upstream; diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 480880e94..cb09b9959 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -3,6 +3,7 @@ use crate::config; use crate::http_proxy; use crate::network_policy::NetworkPolicyDecider; use crate::runtime::unix_socket_permissions_supported; +use crate::socks5; use crate::state::NetworkProxyState; use anyhow::Context; use anyhow::Result; @@ -61,8 +62,9 @@ impl NetworkProxyBuilder { let current_cfg = state.current_cfg().await?; let runtime = config::resolve_runtime(¤t_cfg)?; // Reapply bind clamping for caller overrides so unix-socket proxying stays loopback-only. - let (http_addr, admin_addr) = config::clamp_bind_addrs( + let (http_addr, socks_addr, admin_addr) = config::clamp_bind_addrs( self.http_addr.unwrap_or(runtime.http_addr), + runtime.socks_addr, self.admin_addr.unwrap_or(runtime.admin_addr), ¤t_cfg.network_proxy, ); @@ -70,6 +72,7 @@ impl NetworkProxyBuilder { Ok(NetworkProxy { state, http_addr, + socks_addr, admin_addr, policy_decider: self.policy_decider, }) @@ -80,6 +83,7 @@ impl NetworkProxyBuilder { pub struct NetworkProxy { state: Arc, http_addr: SocketAddr, + socks_addr: SocketAddr, admin_addr: SocketAddr, policy_decider: Option>, } @@ -105,10 +109,21 @@ impl NetworkProxy { self.http_addr, self.policy_decider.clone(), )); + let socks_task = if current_cfg.network_proxy.enable_socks5 { + Some(tokio::spawn(socks5::run_socks5( + self.state.clone(), + self.socks_addr, + self.policy_decider.clone(), + current_cfg.network_proxy.enable_socks5_udp, + ))) + } else { + None + }; let admin_task = tokio::spawn(admin::run_admin_api(self.state.clone(), self.admin_addr)); Ok(NetworkProxyHandle { http_task: Some(http_task), + socks_task, admin_task: Some(admin_task), completed: false, }) @@ -117,6 +132,7 @@ impl NetworkProxy { pub struct NetworkProxyHandle { http_task: Option>>, + socks_task: Option>>, admin_task: Option>>, completed: bool, } @@ -125,6 +141,7 @@ impl NetworkProxyHandle { fn noop() -> Self { Self { http_task: Some(tokio::spawn(async { Ok(()) })), + socks_task: None, admin_task: Some(tokio::spawn(async { Ok(()) })), completed: true, } @@ -133,33 +150,49 @@ impl NetworkProxyHandle { pub async fn wait(mut self) -> Result<()> { let http_task = self.http_task.take().context("missing http proxy task")?; let admin_task = self.admin_task.take().context("missing admin proxy task")?; + let socks_task = self.socks_task.take(); let http_result = http_task.await; let admin_result = admin_task.await; + let socks_result = match socks_task { + Some(task) => Some(task.await), + None => None, + }; self.completed = true; http_result??; admin_result??; + if let Some(socks_result) = socks_result { + socks_result??; + } Ok(()) } pub async fn shutdown(mut self) -> Result<()> { - abort_tasks(self.http_task.take(), self.admin_task.take()).await; + abort_tasks( + self.http_task.take(), + self.socks_task.take(), + self.admin_task.take(), + ) + .await; self.completed = true; Ok(()) } } +async fn abort_task(task: Option>>) { + if let Some(task) = task { + task.abort(); + let _ = task.await; + } +} + async fn abort_tasks( http_task: Option>>, + socks_task: Option>>, admin_task: Option>>, ) { - if let Some(http_task) = http_task { - http_task.abort(); - let _ = http_task.await; - } - if let Some(admin_task) = admin_task { - admin_task.abort(); - let _ = admin_task.await; - } + abort_task(http_task).await; + abort_task(socks_task).await; + abort_task(admin_task).await; } impl Drop for NetworkProxyHandle { @@ -168,9 +201,10 @@ impl Drop for NetworkProxyHandle { return; } let http_task = self.http_task.take(); + let socks_task = self.socks_task.take(); let admin_task = self.admin_task.take(); tokio::spawn(async move { - abort_tasks(http_task, admin_task).await; + abort_tasks(http_task, socks_task, admin_task).await; }); } } diff --git a/codex-rs/network-proxy/src/socks5.rs b/codex-rs/network-proxy/src/socks5.rs new file mode 100644 index 000000000..942650254 --- /dev/null +++ b/codex-rs/network-proxy/src/socks5.rs @@ -0,0 +1,318 @@ +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::reasons::REASON_METHOD_NOT_ALLOWED; +use crate::reasons::REASON_PROXY_DISABLED; +use crate::state::BlockedRequest; +use crate::state::NetworkProxyState; +use anyhow::Context as _; +use anyhow::Result; +use rama_core::Layer; +use rama_core::Service; +use rama_core::error::BoxError; +use rama_core::extensions::ExtensionsRef; +use rama_core::layer::AddInputExtensionLayer; +use rama_core::service::service_fn; +use rama_net::client::EstablishedClientConnection; +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::TcpStream; +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 { handle_socks5_tcp(req, tcp_connector, policy_decider).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 { inspect_socks5_udp(request, udp_state, udp_decider).await } + } + })); + 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(()) +} + +async fn handle_socks5_tcp( + req: TcpRequest, + tcp_connector: TcpConnector, + policy_decider: Option>, +) -> Result, BoxError> { + 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; + if host.is_empty() { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid host").into()); + } + + 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(), + REASON_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(), + REASON_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 +} + +async fn inspect_socks5_udp( + request: RelayRequest, + state: Arc, + policy_decider: Option>, +) -> io::Result { + let RelayRequest { + server_address, + payload, + extensions, + .. + } = request; + + let host = normalize_host(&server_address.ip_addr.to_string()); + let port = server_address.port; + if host.is_empty() { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid host")); + } + + let client = extensions + .get::() + .map(|info| info.peer_addr().to_string()); + + match state.enabled().await { + Ok(true) => {} + Ok(false) => { + let _ = state + .record_blocked(BlockedRequest::new( + host.clone(), + REASON_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 state.network_mode().await { + Ok(NetworkMode::Limited) => { + let _ = state + .record_blocked(BlockedRequest::new( + host.clone(), + REASON_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(&state, policy_decider.as_ref(), &request).await { + Ok(NetworkDecision::Deny { reason }) => { + let _ = 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")) + } + } +}