From db0d8710d5f2df9d36163f253f71a4f9b6e21025 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Fri, 6 Feb 2026 10:46:50 -0800 Subject: [PATCH] feat(network-proxy): add structured policy decision to blocked errors (#10420) ## Summary Add explicit, model-visible network policy decision metadata to blocked proxy responses/errors. Introduces a standardized prefix line: `CODEX_NETWORK_POLICY_DECISION {json}` and wires it through blocked paths for: - HTTP requests - HTTPS CONNECT - SOCKS5 TCP/UDP denials ## Why The model should see *why* a request was blocked (reason/source/protocol/host/port) so it can choose the correct next action. ## Notes - This PR is intentionally independent of config-layering/network-rule runtime integration. - Focus is blocked decision surface only. --- codex-rs/network-proxy/src/http_proxy.rs | 120 ++++++++++++++++--- codex-rs/network-proxy/src/network_policy.rs | 108 +++++++++++++++-- codex-rs/network-proxy/src/responses.rs | 103 +++++++++++++++- codex-rs/network-proxy/src/socks5.rs | 92 +++++++++++--- 4 files changed, 379 insertions(+), 44 deletions(-) diff --git a/codex-rs/network-proxy/src/http_proxy.rs b/codex-rs/network-proxy/src/http_proxy.rs index b2856ac60..08499490f 100644 --- a/codex-rs/network-proxy/src/http_proxy.rs +++ b/codex-rs/network-proxy/src/http_proxy.rs @@ -1,6 +1,8 @@ use crate::config::NetworkMode; use crate::network_policy::NetworkDecision; +use crate::network_policy::NetworkDecisionSource; use crate::network_policy::NetworkPolicyDecider; +use crate::network_policy::NetworkPolicyDecision; use crate::network_policy::NetworkPolicyRequest; use crate::network_policy::NetworkPolicyRequestArgs; use crate::network_policy::NetworkProtocol; @@ -9,8 +11,12 @@ use crate::policy::normalize_host; use crate::reasons::REASON_METHOD_NOT_ALLOWED; use crate::reasons::REASON_NOT_ALLOWED; use crate::reasons::REASON_PROXY_DISABLED; +use crate::responses::PolicyDecisionDetails; use crate::responses::blocked_header_value; +use crate::responses::blocked_message_with_policy; +use crate::responses::blocked_text_response_with_policy; use crate::responses::json_response; +use crate::responses::policy_decision_prefix; use crate::runtime::unix_socket_permissions_supported; use crate::state::BlockedRequest; use crate::state::BlockedRequestArgs; @@ -141,9 +147,10 @@ async fn http_connect_accept( return Err(proxy_disabled_response( &app_state, host, + authority.port, client_addr(&req), Some("CONNECT".to_string()), - "http-connect", + NetworkProtocol::HttpsConnect, ) .await); } @@ -159,7 +166,19 @@ async fn http_connect_accept( }); match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await { - Ok(NetworkDecision::Deny { reason }) => { + Ok(NetworkDecision::Deny { + reason, + source, + decision, + }) => { + let details = PolicyDecisionDetails { + decision, + reason: &reason, + source, + protocol: NetworkProtocol::HttpsConnect, + host: &host, + port: authority.port, + }; let _ = app_state .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), @@ -172,7 +191,7 @@ async fn http_connect_accept( .await; let client = client.as_deref().unwrap_or_default(); warn!("CONNECT blocked (client={client}, host={host}, reason={reason})"); - return Err(blocked_text(&reason)); + return Err(blocked_text_with_details(&reason, &details)); } Ok(NetworkDecision::Allow) => { let client = client.as_deref().unwrap_or_default(); @@ -190,6 +209,14 @@ async fn http_connect_accept( .map_err(|err| internal_error("failed to read network mode", err))?; if mode == NetworkMode::Limited { + let details = PolicyDecisionDetails { + decision: NetworkPolicyDecision::Deny, + reason: REASON_METHOD_NOT_ALLOWED, + source: NetworkDecisionSource::ModeGuard, + protocol: NetworkProtocol::HttpsConnect, + host: &host, + port: authority.port, + }; let _ = app_state .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), @@ -202,7 +229,10 @@ async fn http_connect_accept( .await; let client = client.as_deref().unwrap_or_default(); warn!("CONNECT blocked by method policy (client={client}, host={host}, mode=limited)"); - return Err(blocked_text(REASON_METHOD_NOT_ALLOWED)); + return Err(blocked_text_with_details( + REASON_METHOD_NOT_ALLOWED, + &details, + )); } req.extensions_mut().insert(ProxyTarget(authority)); @@ -346,9 +376,10 @@ async fn http_plain_proxy( return Ok(proxy_disabled_response( &app_state, socket_path, + 0, client_addr(&req), Some(req.method().as_str().to_string()), - "unix-socket", + NetworkProtocol::Http, ) .await); } @@ -358,7 +389,7 @@ async fn http_plain_proxy( warn!( "unix socket blocked by method policy (client={client}, method={method}, mode=limited, allowed_methods=GET, HEAD, OPTIONS)" ); - return Ok(json_blocked("unix-socket", REASON_METHOD_NOT_ALLOWED)); + return Ok(json_blocked("unix-socket", REASON_METHOD_NOT_ALLOWED, None)); } if !unix_socket_permissions_supported() { @@ -387,7 +418,7 @@ async fn http_plain_proxy( Ok(false) => { let client = client.as_deref().unwrap_or_default(); warn!("unix socket blocked (client={client}, path={socket_path})"); - Ok(json_blocked("unix-socket", REASON_NOT_ALLOWED)) + Ok(json_blocked("unix-socket", REASON_NOT_ALLOWED, None)) } Err(err) => { warn!("unix socket check failed: {err}"); @@ -420,9 +451,10 @@ async fn http_plain_proxy( return Ok(proxy_disabled_response( &app_state, host, + port, client_addr(&req), Some(req.method().as_str().to_string()), - "http", + NetworkProtocol::Http, ) .await); } @@ -438,7 +470,19 @@ async fn http_plain_proxy( }); match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await { - Ok(NetworkDecision::Deny { reason }) => { + Ok(NetworkDecision::Deny { + reason, + source, + decision, + }) => { + let details = PolicyDecisionDetails { + decision, + reason: &reason, + source, + protocol: NetworkProtocol::Http, + host: &host, + port, + }; let _ = app_state .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), @@ -451,7 +495,7 @@ async fn http_plain_proxy( .await; let client = client.as_deref().unwrap_or_default(); warn!("request blocked (client={client}, host={host}, reason={reason})"); - return Ok(json_blocked(&host, &reason)); + return Ok(json_blocked(&host, &reason, Some(&details))); } Ok(NetworkDecision::Allow) => {} Err(err) => { @@ -461,6 +505,14 @@ async fn http_plain_proxy( } if !method_allowed { + let details = PolicyDecisionDetails { + decision: NetworkPolicyDecision::Deny, + reason: REASON_METHOD_NOT_ALLOWED, + source: NetworkDecisionSource::ModeGuard, + protocol: NetworkProtocol::Http, + host: &host, + port, + }; let _ = app_state .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), @@ -476,7 +528,11 @@ async fn http_plain_proxy( warn!( "request blocked by method policy (client={client}, host={host}, method={method}, mode=limited, allowed_methods=GET, HEAD, OPTIONS)" ); - return Ok(json_blocked(&host, REASON_METHOD_NOT_ALLOWED)); + return Ok(json_blocked( + &host, + REASON_METHOD_NOT_ALLOWED, + Some(&details), + )); } let client = client.as_deref().unwrap_or_default(); @@ -540,11 +596,21 @@ fn client_addr(input: &T) -> Option { .map(|info| info.peer_addr().to_string()) } -fn json_blocked(host: &str, reason: &str) -> Response { +fn json_blocked(host: &str, reason: &str, details: Option<&PolicyDecisionDetails<'_>>) -> Response { + let (policy_decision_prefix, message) = details + .map(|details| { + ( + Some(policy_decision_prefix(details)), + Some(blocked_message_with_policy(reason, details)), + ) + }) + .unwrap_or((None, None)); let response = BlockedResponse { status: "blocked", host, reason, + policy_decision_prefix, + message, }; let mut resp = json_response(&response); *resp.status_mut() = StatusCode::FORBIDDEN; @@ -555,28 +621,42 @@ fn json_blocked(host: &str, reason: &str) -> Response { resp } -fn blocked_text(reason: &str) -> Response { - crate::responses::blocked_text_response(reason) +fn blocked_text_with_details(reason: &str, details: &PolicyDecisionDetails<'_>) -> Response { + blocked_text_response_with_policy(reason, details) } async fn proxy_disabled_response( app_state: &NetworkProxyState, host: String, + port: u16, client: Option, method: Option, - protocol: &str, + protocol: NetworkProtocol, ) -> Response { + let blocked_host = host.clone(); let _ = app_state .record_blocked(BlockedRequest::new(BlockedRequestArgs { - host, + host: blocked_host, reason: REASON_PROXY_DISABLED.to_string(), client, method, mode: None, - protocol: protocol.to_string(), + protocol: protocol.as_policy_protocol().to_string(), })) .await; - text_response(StatusCode::SERVICE_UNAVAILABLE, "proxy disabled") + + let details = PolicyDecisionDetails { + decision: NetworkPolicyDecision::Deny, + reason: REASON_PROXY_DISABLED, + source: NetworkDecisionSource::ProxyState, + protocol, + host: &host, + port, + }; + text_response( + StatusCode::SERVICE_UNAVAILABLE, + &blocked_message_with_policy(REASON_PROXY_DISABLED, &details), + ) } fn internal_error(context: &str, err: impl std::fmt::Display) -> Response { @@ -597,6 +677,10 @@ struct BlockedResponse<'a> { status: &'static str, host: &'a str, reason: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + policy_decision_prefix: Option, + #[serde(skip_serializing_if = "Option::is_none")] + message: Option, } #[cfg(test)] diff --git a/codex-rs/network-proxy/src/network_policy.rs b/codex-rs/network-proxy/src/network_policy.rs index f14202510..aa79acbd1 100644 --- a/codex-rs/network-proxy/src/network_policy.rs +++ b/codex-rs/network-proxy/src/network_policy.rs @@ -15,6 +15,51 @@ pub enum NetworkProtocol { Socks5Udp, } +impl NetworkProtocol { + pub const fn as_policy_protocol(self) -> &'static str { + match self { + Self::Http => "http", + Self::HttpsConnect => "https_connect", + Self::Socks5Tcp => "socks5_tcp", + Self::Socks5Udp => "socks5_udp", + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NetworkPolicyDecision { + Deny, + Ask, +} + +impl NetworkPolicyDecision { + pub const fn as_str(self) -> &'static str { + match self { + Self::Deny => "deny", + Self::Ask => "ask", + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NetworkDecisionSource { + BaselinePolicy, + ModeGuard, + ProxyState, + Decider, +} + +impl NetworkDecisionSource { + pub const fn as_str(self) -> &'static str { + match self { + Self::BaselinePolicy => "baseline_policy", + Self::ModeGuard => "mode_guard", + Self::ProxyState => "proxy_state", + Self::Decider => "decider", + } + } +} + #[derive(Clone, Debug)] pub struct NetworkPolicyRequest { pub protocol: NetworkProtocol, @@ -62,18 +107,44 @@ impl NetworkPolicyRequest { #[derive(Clone, Debug, PartialEq, Eq)] pub enum NetworkDecision { Allow, - Deny { reason: String }, + Deny { + reason: String, + source: NetworkDecisionSource, + decision: NetworkPolicyDecision, + }, } impl NetworkDecision { pub fn deny(reason: impl Into) -> Self { + Self::deny_with_source(reason, NetworkDecisionSource::Decider) + } + + pub fn deny_with_source(reason: impl Into, source: NetworkDecisionSource) -> Self { let reason = reason.into(); let reason = if reason.is_empty() { REASON_POLICY_DENIED.to_string() } else { reason }; - Self::Deny { reason } + Self::Deny { + reason, + source, + decision: NetworkPolicyDecision::Deny, + } + } + + pub fn ask_with_source(reason: impl Into, source: NetworkDecisionSource) -> Self { + let reason = reason.into(); + let reason = if reason.is_empty() { + REASON_POLICY_DENIED.to_string() + } else { + reason + }; + Self::Deny { + reason, + source, + decision: NetworkPolicyDecision::Ask, + } } } @@ -114,12 +185,31 @@ pub(crate) async fn evaluate_host_policy( HostBlockDecision::Allowed => Ok(NetworkDecision::Allow), HostBlockDecision::Blocked(HostBlockReason::NotAllowed) => { if let Some(decider) = decider { - Ok(decider.decide(request.clone()).await) + Ok(map_decider_decision(decider.decide(request.clone()).await)) } else { - Ok(NetworkDecision::deny(HostBlockReason::NotAllowed.as_str())) + Ok(NetworkDecision::deny_with_source( + HostBlockReason::NotAllowed.as_str(), + NetworkDecisionSource::BaselinePolicy, + )) } } - HostBlockDecision::Blocked(reason) => Ok(NetworkDecision::deny(reason.as_str())), + HostBlockDecision::Blocked(reason) => Ok(NetworkDecision::deny_with_source( + reason.as_str(), + NetworkDecisionSource::BaselinePolicy, + )), + } +} + +fn map_decider_decision(decision: NetworkDecision) -> NetworkDecision { + match decision { + NetworkDecision::Allow => NetworkDecision::Allow, + NetworkDecision::Deny { + reason, decision, .. + } => NetworkDecision::Deny { + reason, + source: NetworkDecisionSource::Decider, + decision, + }, } } @@ -199,7 +289,9 @@ mod tests { assert_eq!( decision, NetworkDecision::Deny { - reason: REASON_DENIED.to_string() + reason: REASON_DENIED.to_string(), + source: NetworkDecisionSource::BaselinePolicy, + decision: NetworkPolicyDecision::Deny, } ); assert_eq!(calls.load(Ordering::SeqCst), 0); @@ -237,7 +329,9 @@ mod tests { assert_eq!( decision, NetworkDecision::Deny { - reason: REASON_NOT_ALLOWED_LOCAL.to_string() + reason: REASON_NOT_ALLOWED_LOCAL.to_string(), + source: NetworkDecisionSource::BaselinePolicy, + decision: NetworkPolicyDecision::Deny, } ); assert_eq!(calls.load(Ordering::SeqCst), 0); diff --git a/codex-rs/network-proxy/src/responses.rs b/codex-rs/network-proxy/src/responses.rs index d02d1910e..354d4bbaf 100644 --- a/codex-rs/network-proxy/src/responses.rs +++ b/codex-rs/network-proxy/src/responses.rs @@ -1,3 +1,6 @@ +use crate::network_policy::NetworkDecisionSource; +use crate::network_policy::NetworkPolicyDecision; +use crate::network_policy::NetworkProtocol; use crate::reasons::REASON_DENIED; use crate::reasons::REASON_METHOD_NOT_ALLOWED; use crate::reasons::REASON_NOT_ALLOWED; @@ -8,6 +11,28 @@ use rama_http::StatusCode; use serde::Serialize; use tracing::error; +const NETWORK_POLICY_DECISION_PREFIX: &str = "CODEX_NETWORK_POLICY_DECISION"; + +pub struct PolicyDecisionDetails<'a> { + pub decision: NetworkPolicyDecision, + pub reason: &'a str, + pub source: NetworkDecisionSource, + pub protocol: NetworkProtocol, + pub host: &'a str, + pub port: u16, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct PolicyDecisionPayload<'a> { + decision: &'a str, + reason: &'a str, + source: &'a str, + protocol: &'a str, + host: &'a str, + port: u16, +} + pub fn text_response(status: StatusCode, body: &str) -> Response { Response::builder() .status(status) @@ -57,11 +82,85 @@ pub fn blocked_message(reason: &str) -> &'static str { } } -pub fn blocked_text_response(reason: &str) -> Response { +pub fn policy_decision_prefix(details: &PolicyDecisionDetails<'_>) -> String { + let payload = PolicyDecisionPayload { + decision: details.decision.as_str(), + reason: details.reason, + source: details.source.as_str(), + protocol: details.protocol.as_policy_protocol(), + host: details.host, + port: details.port, + }; + let payload_json = match serde_json::to_string(&payload) { + Ok(json) => json, + Err(err) => { + error!("failed to serialize policy decision payload: {err}"); + "{}".to_string() + } + }; + format!("{NETWORK_POLICY_DECISION_PREFIX} {payload_json}") +} + +pub fn blocked_message_with_policy(reason: &str, details: &PolicyDecisionDetails<'_>) -> String { + format!( + "{}\n{}", + policy_decision_prefix(details), + blocked_message(reason) + ) +} + +pub fn blocked_text_response_with_policy( + reason: &str, + details: &PolicyDecisionDetails<'_>, +) -> Response { Response::builder() .status(StatusCode::FORBIDDEN) .header("content-type", "text/plain") .header("x-proxy-error", blocked_header_value(reason)) - .body(Body::from(blocked_message(reason))) + .body(Body::from(blocked_message_with_policy(reason, details))) .unwrap_or_else(|_| Response::new(Body::from("blocked"))) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::reasons::REASON_NOT_ALLOWED; + use pretty_assertions::assert_eq; + + #[test] + fn policy_decision_prefix_serializes_expected_payload() { + let details = PolicyDecisionDetails { + decision: NetworkPolicyDecision::Ask, + reason: REASON_NOT_ALLOWED, + source: NetworkDecisionSource::Decider, + protocol: NetworkProtocol::HttpsConnect, + host: "api.example.com", + port: 443, + }; + + let line = policy_decision_prefix(&details); + assert_eq!( + line, + r#"CODEX_NETWORK_POLICY_DECISION {"decision":"ask","reason":"not_allowed","source":"decider","protocol":"https_connect","host":"api.example.com","port":443}"# + ); + } + + #[test] + fn blocked_message_with_policy_includes_prefix_and_human_message() { + let details = PolicyDecisionDetails { + decision: NetworkPolicyDecision::Deny, + reason: REASON_NOT_ALLOWED, + source: NetworkDecisionSource::BaselinePolicy, + protocol: NetworkProtocol::Http, + host: "api.example.com", + port: 80, + }; + + let message = blocked_message_with_policy(REASON_NOT_ALLOWED, &details); + assert_eq!( + message, + r#"CODEX_NETWORK_POLICY_DECISION {"decision":"deny","reason":"not_allowed","source":"baseline_policy","protocol":"http","host":"api.example.com","port":80} +Codex blocked this request: domain not in allowlist."# + ); + } +} diff --git a/codex-rs/network-proxy/src/socks5.rs b/codex-rs/network-proxy/src/socks5.rs index 44be9060b..33f7416c4 100644 --- a/codex-rs/network-proxy/src/socks5.rs +++ b/codex-rs/network-proxy/src/socks5.rs @@ -1,6 +1,8 @@ use crate::config::NetworkMode; use crate::network_policy::NetworkDecision; +use crate::network_policy::NetworkDecisionSource; use crate::network_policy::NetworkPolicyDecider; +use crate::network_policy::NetworkPolicyDecision; use crate::network_policy::NetworkPolicyRequest; use crate::network_policy::NetworkPolicyRequestArgs; use crate::network_policy::NetworkProtocol; @@ -8,6 +10,8 @@ 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::responses::PolicyDecisionDetails; +use crate::responses::blocked_message_with_policy; use crate::state::BlockedRequest; use crate::state::BlockedRequestArgs; use crate::state::NetworkProxyState; @@ -123,6 +127,14 @@ async fn handle_socks5_tcp( match app_state.enabled().await { Ok(true) => {} Ok(false) => { + let details = PolicyDecisionDetails { + decision: NetworkPolicyDecision::Deny, + reason: REASON_PROXY_DISABLED, + source: NetworkDecisionSource::ProxyState, + protocol: NetworkProtocol::Socks5Tcp, + host: &host, + port, + }; let _ = app_state .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), @@ -135,7 +147,7 @@ async fn handle_socks5_tcp( .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()); + return Err(policy_denied_error(REASON_PROXY_DISABLED, &details).into()); } Err(err) => { error!("failed to read enabled state: {err}"); @@ -145,6 +157,14 @@ async fn handle_socks5_tcp( match app_state.network_mode().await { Ok(NetworkMode::Limited) => { + let details = PolicyDecisionDetails { + decision: NetworkPolicyDecision::Deny, + reason: REASON_METHOD_NOT_ALLOWED, + source: NetworkDecisionSource::ModeGuard, + protocol: NetworkProtocol::Socks5Tcp, + host: &host, + port, + }; let _ = app_state .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), @@ -159,7 +179,7 @@ async fn handle_socks5_tcp( 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()); + return Err(policy_denied_error(REASON_METHOD_NOT_ALLOWED, &details).into()); } Ok(NetworkMode::Full) => {} Err(err) => { @@ -179,7 +199,19 @@ async fn handle_socks5_tcp( }); match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await { - Ok(NetworkDecision::Deny { reason }) => { + Ok(NetworkDecision::Deny { + reason, + source, + decision, + }) => { + let details = PolicyDecisionDetails { + decision, + reason: &reason, + source, + protocol: NetworkProtocol::Socks5Tcp, + host: &host, + port, + }; let _ = app_state .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), @@ -192,7 +224,7 @@ async fn handle_socks5_tcp( .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()); + return Err(policy_denied_error(&reason, &details).into()); } Ok(NetworkDecision::Allow) => { let client = client.as_deref().unwrap_or_default(); @@ -232,6 +264,14 @@ async fn inspect_socks5_udp( match state.enabled().await { Ok(true) => {} Ok(false) => { + let details = PolicyDecisionDetails { + decision: NetworkPolicyDecision::Deny, + reason: REASON_PROXY_DISABLED, + source: NetworkDecisionSource::ProxyState, + protocol: NetworkProtocol::Socks5Udp, + host: &host, + port, + }; let _ = state .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), @@ -244,10 +284,7 @@ async fn inspect_socks5_udp( .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, - }); + return Err(policy_denied_error(REASON_PROXY_DISABLED, &details)); } Err(err) => { error!("failed to read enabled state: {err}"); @@ -257,6 +294,14 @@ async fn inspect_socks5_udp( match state.network_mode().await { Ok(NetworkMode::Limited) => { + let details = PolicyDecisionDetails { + decision: NetworkPolicyDecision::Deny, + reason: REASON_METHOD_NOT_ALLOWED, + source: NetworkDecisionSource::ModeGuard, + protocol: NetworkProtocol::Socks5Udp, + host: &host, + port, + }; let _ = state .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), @@ -267,10 +312,7 @@ async fn inspect_socks5_udp( protocol: "socks5-udp".to_string(), })) .await; - return Ok(RelayResponse { - maybe_payload: None, - extensions, - }); + return Err(policy_denied_error(REASON_METHOD_NOT_ALLOWED, &details)); } Ok(NetworkMode::Full) => {} Err(err) => { @@ -290,7 +332,19 @@ async fn inspect_socks5_udp( }); match evaluate_host_policy(&state, policy_decider.as_ref(), &request).await { - Ok(NetworkDecision::Deny { reason }) => { + Ok(NetworkDecision::Deny { + reason, + source, + decision, + }) => { + let details = PolicyDecisionDetails { + decision, + reason: &reason, + source, + protocol: NetworkProtocol::Socks5Udp, + host: &host, + port, + }; let _ = state .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), @@ -303,10 +357,7 @@ async fn inspect_socks5_udp( .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, - }) + Err(policy_denied_error(&reason, &details)) } Ok(NetworkDecision::Allow) => Ok(RelayResponse { maybe_payload: Some(payload), @@ -318,3 +369,10 @@ async fn inspect_socks5_udp( } } } + +fn policy_denied_error(reason: &str, details: &PolicyDecisionDetails<'_>) -> io::Error { + io::Error::new( + io::ErrorKind::PermissionDenied, + blocked_message_with_policy(reason, details), + ) +}