mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
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:
@@ -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)]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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."#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user