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.
This commit is contained in:
viyatb-oai
2026-02-06 10:46:50 -08:00
committed by GitHub
parent 36c16e0c58
commit db0d8710d5
4 changed files with 379 additions and 44 deletions

View File

@@ -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<T: ExtensionsRef>(input: &T) -> Option<String> {
.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<String>,
method: Option<String>,
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<String>,
}
#[cfg(test)]

View File

@@ -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<String>) -> Self {
Self::deny_with_source(reason, NetworkDecisionSource::Decider)
}
pub fn deny_with_source(reason: impl Into<String>, 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<String>, 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);

View File

@@ -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."#
);
}
}

View File

@@ -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),
)
}