feat(network-proxy): structured policy signaling and attempt correlation to core (#11662)

## Summary
When network requests were blocked, downstream code often had to infer
ask vs deny from free-form response text. That was brittle and led to
incorrect approval behavior.
This PR fixes the proxy side so blocked decisions are structured and
request metadata survives reliably.

## Description
- Blocked proxy responses now carry consistent structured policy
decision data.
- Request attempt metadata is preserved across proxy env paths
(including ALL_PROXY flows).
- Header stripping was tightened so we still remove unsafe forwarding
headers, but keep metadata needed for policy handling.
- Block messages were clarified (for example, allowlist miss vs explicit
deny).
- Added unified violation log entries so policy failures can be
inspected in one place.
- Added/updated tests for these behaviors.

---------

Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
This commit is contained in:
viyatb-oai
2026-02-13 01:01:11 -08:00
committed by GitHub
parent fca5629e34
commit 2bced810da
14 changed files with 581 additions and 83 deletions

View File

@@ -1,3 +1,5 @@
iTerm iTerm
iTerm2 iTerm2
psuedo psuedo
te
TE

View File

@@ -3,4 +3,4 @@
skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl,frame*.txt,*.snap,*.snap.new,*meriyah.umd.min.js skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl,frame*.txt,*.snap,*.snap.new,*meriyah.umd.min.js
check-hidden = true check-hidden = true
ignore-regex = ^\s*"image/\S+": ".*|\b(afterAll)\b ignore-regex = ^\s*"image/\S+": ".*|\b(afterAll)\b
ignore-words-list = ratatui,ser,iTerm,iterm2,iterm ignore-words-list = ratatui,ser,iTerm,iterm2,iterm,te,TE

1
codex-rs/Cargo.lock generated
View File

@@ -2035,6 +2035,7 @@ version = "0.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"base64 0.22.1",
"clap", "clap",
"codex-utils-absolute-path", "codex-utils-absolute-path",
"codex-utils-rustls-provider", "codex-utils-rustls-provider",

View File

@@ -14,6 +14,7 @@ workspace = true
[dependencies] [dependencies]
anyhow = { workspace = true } anyhow = { workspace = true }
async-trait = { workspace = true } async-trait = { workspace = true }
base64 = { workspace = true }
clap = { workspace = true, features = ["derive"] } clap = { workspace = true, features = ["derive"] }
codex-utils-absolute-path = { workspace = true } codex-utils-absolute-path = { workspace = true }
codex-utils-rustls-provider = { workspace = true } codex-utils-rustls-provider = { workspace = true }

View File

@@ -89,7 +89,7 @@ async fn handle_admin_request(
text_response(StatusCode::INTERNAL_SERVER_ERROR, "error") text_response(StatusCode::INTERNAL_SERVER_ERROR, "error")
} }
}, },
("GET", "/blocked") => match state.drain_blocked().await { ("GET", "/blocked") => match state.blocked_snapshot().await {
Ok(blocked) => json_response(&BlockedResponse { blocked }), Ok(blocked) => json_response(&BlockedResponse { blocked }),
Err(err) => { Err(err) => {
error!("failed to read blocked queue: {err}"); error!("failed to read blocked queue: {err}");

View File

@@ -1,4 +1,5 @@
use crate::config::NetworkMode; use crate::config::NetworkMode;
use crate::metadata::attempt_id_from_proxy_authorization;
use crate::network_policy::NetworkDecision; use crate::network_policy::NetworkDecision;
use crate::network_policy::NetworkDecisionSource; use crate::network_policy::NetworkDecisionSource;
use crate::network_policy::NetworkPolicyDecider; use crate::network_policy::NetworkPolicyDecider;
@@ -16,7 +17,6 @@ use crate::responses::blocked_header_value;
use crate::responses::blocked_message_with_policy; use crate::responses::blocked_message_with_policy;
use crate::responses::blocked_text_response_with_policy; use crate::responses::blocked_text_response_with_policy;
use crate::responses::json_response; use crate::responses::json_response;
use crate::responses::policy_decision_prefix;
use crate::runtime::unix_socket_permissions_supported; use crate::runtime::unix_socket_permissions_supported;
use crate::state::BlockedRequest; use crate::state::BlockedRequest;
use crate::state::BlockedRequestArgs; use crate::state::BlockedRequestArgs;
@@ -36,11 +36,13 @@ use rama_core::layer::AddInputExtensionLayer;
use rama_core::rt::Executor; use rama_core::rt::Executor;
use rama_core::service::service_fn; use rama_core::service::service_fn;
use rama_http::Body; use rama_http::Body;
use rama_http::HeaderMap;
use rama_http::HeaderName;
use rama_http::HeaderValue; use rama_http::HeaderValue;
use rama_http::Request; use rama_http::Request;
use rama_http::Response; use rama_http::Response;
use rama_http::StatusCode; use rama_http::StatusCode;
use rama_http::layer::remove_header::RemoveRequestHeaderLayer; use rama_http::header;
use rama_http::layer::remove_header::RemoveResponseHeaderLayer; use rama_http::layer::remove_header::RemoveResponseHeaderLayer;
use rama_http::matcher::MethodMatcher; use rama_http::matcher::MethodMatcher;
use rama_http_backend::client::proxy::layer::HttpProxyConnector; use rama_http_backend::client::proxy::layer::HttpProxyConnector;
@@ -119,7 +121,6 @@ async fn run_http_proxy_with_listener(
service_fn(http_connect_proxy), service_fn(http_connect_proxy),
), ),
RemoveResponseHeaderLayer::hop_by_hop(), RemoveResponseHeaderLayer::hop_by_hop(),
RemoveRequestHeaderLayer::hop_by_hop(),
) )
.into_layer(service_fn({ .into_layer(service_fn({
let policy_decider = policy_decider.clone(); let policy_decider = policy_decider.clone();
@@ -159,6 +160,7 @@ async fn http_connect_accept(
} }
let client = client_addr(&req); let client = client_addr(&req);
let network_attempt_id = request_network_attempt_id(&req);
let enabled = app_state let enabled = app_state
.enabled() .enabled()
@@ -186,6 +188,7 @@ async fn http_connect_accept(
method: Some("CONNECT".to_string()), method: Some("CONNECT".to_string()),
command: None, command: None,
exec_policy_hint: None, exec_policy_hint: None,
attempt_id: network_attempt_id.clone(),
}); });
match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await { match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await {
@@ -210,6 +213,10 @@ async fn http_connect_accept(
method: Some("CONNECT".to_string()), method: Some("CONNECT".to_string()),
mode: None, mode: None,
protocol: "http-connect".to_string(), protocol: "http-connect".to_string(),
attempt_id: network_attempt_id.clone(),
decision: Some(details.decision.as_str().to_string()),
source: Some(details.source.as_str().to_string()),
port: Some(authority.port),
})) }))
.await; .await;
let client = client.as_deref().unwrap_or_default(); let client = client.as_deref().unwrap_or_default();
@@ -248,6 +255,10 @@ async fn http_connect_accept(
method: Some("CONNECT".to_string()), method: Some("CONNECT".to_string()),
mode: Some(NetworkMode::Limited), mode: Some(NetworkMode::Limited),
protocol: "http-connect".to_string(), protocol: "http-connect".to_string(),
attempt_id: network_attempt_id,
decision: Some(details.decision.as_str().to_string()),
source: Some(details.source.as_str().to_string()),
port: Some(authority.port),
})) }))
.await; .await;
let client = client.as_deref().unwrap_or_default(); let client = client.as_deref().unwrap_or_default();
@@ -353,7 +364,7 @@ async fn forward_connect_tunnel(
async fn http_plain_proxy( async fn http_plain_proxy(
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>, policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
req: Request, mut req: Request,
) -> Result<Response, Infallible> { ) -> Result<Response, Infallible> {
let app_state = match req.extensions().get::<Arc<NetworkProxyState>>().cloned() { let app_state = match req.extensions().get::<Arc<NetworkProxyState>>().cloned() {
Some(state) => state, Some(state) => state,
@@ -363,6 +374,7 @@ async fn http_plain_proxy(
} }
}; };
let client = client_addr(&req); let client = client_addr(&req);
let network_attempt_id = request_network_attempt_id(&req);
let method_allowed = match app_state let method_allowed = match app_state
.method_allowed(req.method().as_str()) .method_allowed(req.method().as_str())
@@ -492,6 +504,7 @@ async fn http_plain_proxy(
method: Some(req.method().as_str().to_string()), method: Some(req.method().as_str().to_string()),
command: None, command: None,
exec_policy_hint: None, exec_policy_hint: None,
attempt_id: network_attempt_id.clone(),
}); });
match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await { match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await {
@@ -516,6 +529,10 @@ async fn http_plain_proxy(
method: Some(req.method().as_str().to_string()), method: Some(req.method().as_str().to_string()),
mode: None, mode: None,
protocol: "http".to_string(), protocol: "http".to_string(),
attempt_id: network_attempt_id.clone(),
decision: Some(details.decision.as_str().to_string()),
source: Some(details.source.as_str().to_string()),
port: Some(port),
})) }))
.await; .await;
let client = client.as_deref().unwrap_or_default(); let client = client.as_deref().unwrap_or_default();
@@ -546,6 +563,10 @@ async fn http_plain_proxy(
method: Some(req.method().as_str().to_string()), method: Some(req.method().as_str().to_string()),
mode: Some(NetworkMode::Limited), mode: Some(NetworkMode::Limited),
protocol: "http".to_string(), protocol: "http".to_string(),
attempt_id: network_attempt_id,
decision: Some(details.decision.as_str().to_string()),
source: Some(details.source.as_str().to_string()),
port: Some(port),
})) }))
.await; .await;
let client = client.as_deref().unwrap_or_default(); let client = client.as_deref().unwrap_or_default();
@@ -578,6 +599,8 @@ async fn http_plain_proxy(
UpstreamClient::direct() UpstreamClient::direct()
}; };
// Strip hop-by-hop headers only after extracting metadata used for policy correlation.
remove_hop_by_hop_request_headers(req.headers_mut());
match client.serve(req).await { match client.serve(req).await {
Ok(resp) => Ok(resp), Ok(resp) => Ok(resp),
Err(err) => { Err(err) => {
@@ -602,6 +625,7 @@ async fn proxy_via_unix_socket(req: Request, socket_path: &str) -> Result<Respon
.parse() .parse()
.with_context(|| format!("invalid unix socket request path: {path}"))?; .with_context(|| format!("invalid unix socket request path: {path}"))?;
parts.headers.remove("x-unix-socket"); parts.headers.remove("x-unix-socket");
remove_hop_by_hop_request_headers(&mut parts.headers);
let req = Request::from_parts(parts, body); let req = Request::from_parts(parts, body);
client.serve(req).await.map_err(anyhow::Error::from) client.serve(req).await.map_err(anyhow::Error::from)
@@ -621,20 +645,67 @@ fn client_addr<T: ExtensionsRef>(input: &T) -> Option<String> {
.map(|info| info.peer_addr().to_string()) .map(|info| info.peer_addr().to_string())
} }
fn request_network_attempt_id(req: &Request) -> Option<String> {
// Some HTTP stacks normalize proxy credentials into `authorization`; accept both.
attempt_id_from_proxy_authorization(req.headers().get("proxy-authorization"))
.or_else(|| attempt_id_from_proxy_authorization(req.headers().get("authorization")))
}
fn remove_hop_by_hop_request_headers(headers: &mut HeaderMap) {
while let Some(raw_connection) = headers.get(header::CONNECTION).cloned() {
headers.remove(header::CONNECTION);
if let Ok(raw_connection) = raw_connection.to_str() {
let connection_headers: Vec<String> = raw_connection
.split(',')
.map(str::trim)
.filter(|token| !token.is_empty())
.map(ToOwned::to_owned)
.collect();
for token in connection_headers {
if let Ok(name) = HeaderName::from_bytes(token.as_bytes()) {
headers.remove(name);
}
}
}
}
for name in [
&header::KEEP_ALIVE,
&header::PROXY_CONNECTION,
&header::PROXY_AUTHORIZATION,
&header::TRAILER,
&header::TRANSFER_ENCODING,
&header::UPGRADE,
] {
headers.remove(name);
}
// codespell:ignore te,TE
// 0x74,0x65 is ASCII "te" (the HTTP TE hop-by-hop header).
if let Ok(short_hop_header_name) = HeaderName::from_bytes(&[0x74, 0x65]) {
headers.remove(short_hop_header_name);
}
}
fn json_blocked(host: &str, reason: &str, details: Option<&PolicyDecisionDetails<'_>>) -> Response { fn json_blocked(host: &str, reason: &str, details: Option<&PolicyDecisionDetails<'_>>) -> Response {
let (policy_decision_prefix, message) = details let (message, decision, source, protocol, port) = details
.map(|details| { .map(|details| {
( (
Some(policy_decision_prefix(details)),
Some(blocked_message_with_policy(reason, details)), Some(blocked_message_with_policy(reason, details)),
Some(details.decision.as_str()),
Some(details.source.as_str()),
Some(details.protocol.as_policy_protocol()),
Some(details.port),
) )
}) })
.unwrap_or((None, None)); .unwrap_or((None, None, None, None, None));
let response = BlockedResponse { let response = BlockedResponse {
status: "blocked", status: "blocked",
host, host,
reason, reason,
policy_decision_prefix, decision,
source,
protocol,
port,
message, message,
}; };
let mut resp = json_response(&response); let mut resp = json_response(&response);
@@ -667,6 +738,10 @@ async fn proxy_disabled_response(
method, method,
mode: None, mode: None,
protocol: protocol.as_policy_protocol().to_string(), protocol: protocol.as_policy_protocol().to_string(),
attempt_id: None,
decision: Some("deny".to_string()),
source: Some("proxy_state".to_string()),
port: Some(port),
})) }))
.await; .await;
@@ -703,7 +778,13 @@ struct BlockedResponse<'a> {
host: &'a str, host: &'a str,
reason: &'a str, reason: &'a str,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
policy_decision_prefix: Option<String>, decision: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
protocol: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
message: Option<String>, message: Option<String>,
} }
@@ -715,6 +796,8 @@ mod tests {
use crate::config::NetworkMode; use crate::config::NetworkMode;
use crate::config::NetworkProxySettings; use crate::config::NetworkProxySettings;
use crate::runtime::network_proxy_state_for_policy; use crate::runtime::network_proxy_state_for_policy;
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rama_http::Method; use rama_http::Method;
use rama_http::Request; use rama_http::Request;
@@ -744,4 +827,67 @@ mod tests {
"blocked-by-method-policy" "blocked-by-method-policy"
); );
} }
#[test]
fn request_network_attempt_id_reads_proxy_authorization_header() {
let encoded = STANDARD.encode("codex-net-attempt-attempt-1:");
let req = Request::builder()
.method(Method::GET)
.uri("http://example.com")
.header("proxy-authorization", format!("Basic {encoded}"))
.body(Body::empty())
.unwrap();
assert_eq!(
request_network_attempt_id(&req),
Some("attempt-1".to_string())
);
}
#[test]
fn request_network_attempt_id_reads_authorization_header_fallback() {
let encoded = STANDARD.encode("codex-net-attempt-attempt-2:");
let req = Request::builder()
.method(Method::GET)
.uri("http://example.com")
.header("authorization", format!("Basic {encoded}"))
.body(Body::empty())
.unwrap();
assert_eq!(
request_network_attempt_id(&req),
Some("attempt-2".to_string())
);
}
#[test]
fn remove_hop_by_hop_request_headers_keeps_forwarding_headers() {
let mut headers = HeaderMap::new();
headers.insert(
header::CONNECTION,
HeaderValue::from_static("x-hop, keep-alive"),
);
headers.insert("x-hop", HeaderValue::from_static("1"));
headers.insert(
header::PROXY_AUTHORIZATION,
HeaderValue::from_static("Basic abc"),
);
headers.insert(
&header::X_FORWARDED_FOR,
HeaderValue::from_static("127.0.0.1"),
);
headers.insert(header::HOST, HeaderValue::from_static("example.com"));
remove_hop_by_hop_request_headers(&mut headers);
assert_eq!(headers.get(header::CONNECTION), None);
assert_eq!(headers.get("x-hop"), None);
assert_eq!(headers.get(header::PROXY_AUTHORIZATION), None);
assert_eq!(
headers.get(&header::X_FORWARDED_FOR),
Some(&HeaderValue::from_static("127.0.0.1"))
);
assert_eq!(
headers.get(header::HOST),
Some(&HeaderValue::from_static("example.com"))
);
}
} }

View File

@@ -3,6 +3,7 @@
mod admin; mod admin;
mod config; mod config;
mod http_proxy; mod http_proxy;
mod metadata;
mod network_policy; mod network_policy;
mod policy; mod policy;
mod proxy; mod proxy;
@@ -32,6 +33,7 @@ pub use proxy::NetworkProxyHandle;
pub use proxy::PROXY_URL_ENV_KEYS; pub use proxy::PROXY_URL_ENV_KEYS;
pub use proxy::has_proxy_url_env_vars; pub use proxy::has_proxy_url_env_vars;
pub use proxy::proxy_url_env_value; pub use proxy::proxy_url_env_value;
pub use runtime::BlockedRequest;
pub use runtime::ConfigReloader; pub use runtime::ConfigReloader;
pub use runtime::ConfigState; pub use runtime::ConfigState;
pub use runtime::NetworkProxyState; pub use runtime::NetworkProxyState;

View File

@@ -0,0 +1,50 @@
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use rama_http::HeaderValue;
pub const NETWORK_ATTEMPT_USERNAME_PREFIX: &str = "codex-net-attempt-";
pub fn proxy_username_for_attempt_id(attempt_id: &str) -> String {
format!("{NETWORK_ATTEMPT_USERNAME_PREFIX}{attempt_id}")
}
pub fn attempt_id_from_proxy_authorization(header: Option<&HeaderValue>) -> Option<String> {
let header = header?;
let raw = header.to_str().ok()?;
let encoded = raw.strip_prefix("Basic ")?;
let decoded = STANDARD.decode(encoded.trim()).ok()?;
let decoded = String::from_utf8(decoded).ok()?;
let username = decoded
.split_once(':')
.map(|(user, _)| user)
.unwrap_or(decoded.as_str());
let attempt_id = username.strip_prefix(NETWORK_ATTEMPT_USERNAME_PREFIX)?;
if attempt_id.is_empty() {
None
} else {
Some(attempt_id.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use base64::engine::general_purpose::STANDARD;
#[test]
fn parses_attempt_id_from_proxy_authorization_header() {
let encoded = STANDARD.encode(format!("{NETWORK_ATTEMPT_USERNAME_PREFIX}abc123:"));
let header = HeaderValue::from_str(&format!("Basic {encoded}")).unwrap();
assert_eq!(
attempt_id_from_proxy_authorization(Some(&header)),
Some("abc123".to_string())
);
}
#[test]
fn ignores_non_attempt_proxy_authorization_header() {
let encoded = STANDARD.encode("normal-user:password");
let header = HeaderValue::from_str(&format!("Basic {encoded}")).unwrap();
assert_eq!(attempt_id_from_proxy_authorization(Some(&header)), None);
}
}

View File

@@ -69,6 +69,7 @@ pub struct NetworkPolicyRequest {
pub method: Option<String>, pub method: Option<String>,
pub command: Option<String>, pub command: Option<String>,
pub exec_policy_hint: Option<String>, pub exec_policy_hint: Option<String>,
pub attempt_id: Option<String>,
} }
pub struct NetworkPolicyRequestArgs { pub struct NetworkPolicyRequestArgs {
@@ -79,6 +80,7 @@ pub struct NetworkPolicyRequestArgs {
pub method: Option<String>, pub method: Option<String>,
pub command: Option<String>, pub command: Option<String>,
pub exec_policy_hint: Option<String>, pub exec_policy_hint: Option<String>,
pub attempt_id: Option<String>,
} }
impl NetworkPolicyRequest { impl NetworkPolicyRequest {
@@ -91,6 +93,7 @@ impl NetworkPolicyRequest {
method, method,
command, command,
exec_policy_hint, exec_policy_hint,
attempt_id,
} = args; } = args;
Self { Self {
protocol, protocol,
@@ -100,6 +103,7 @@ impl NetworkPolicyRequest {
method, method,
command, command,
exec_policy_hint, exec_policy_hint,
attempt_id,
} }
} }
} }
@@ -119,6 +123,10 @@ impl NetworkDecision {
Self::deny_with_source(reason, NetworkDecisionSource::Decider) Self::deny_with_source(reason, NetworkDecisionSource::Decider)
} }
pub fn ask(reason: impl Into<String>) -> Self {
Self::ask_with_source(reason, NetworkDecisionSource::Decider)
}
pub fn deny_with_source(reason: impl Into<String>, source: NetworkDecisionSource) -> Self { pub fn deny_with_source(reason: impl Into<String>, source: NetworkDecisionSource) -> Self {
let reason = reason.into(); let reason = reason.into();
let reason = if reason.is_empty() { let reason = if reason.is_empty() {
@@ -216,9 +224,9 @@ fn map_decider_decision(decision: NetworkDecision) -> NetworkDecision {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::config::NetworkProxySettings; use crate::config::NetworkProxySettings;
use crate::reasons::REASON_DENIED; use crate::reasons::REASON_DENIED;
use crate::reasons::REASON_NOT_ALLOWED;
use crate::reasons::REASON_NOT_ALLOWED_LOCAL; use crate::reasons::REASON_NOT_ALLOWED_LOCAL;
use crate::state::network_proxy_state_for_policy; use crate::state::network_proxy_state_for_policy;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -248,6 +256,7 @@ mod tests {
method: Some("GET".to_string()), method: Some("GET".to_string()),
command: None, command: None,
exec_policy_hint: None, exec_policy_hint: None,
attempt_id: None,
}); });
let decision = evaluate_host_policy(&state, Some(&decider), &request) let decision = evaluate_host_policy(&state, Some(&decider), &request)
@@ -281,6 +290,7 @@ mod tests {
method: Some("GET".to_string()), method: Some("GET".to_string()),
command: None, command: None,
exec_policy_hint: None, exec_policy_hint: None,
attempt_id: None,
}); });
let decision = evaluate_host_policy(&state, Some(&decider), &request) let decision = evaluate_host_policy(&state, Some(&decider), &request)
@@ -321,6 +331,7 @@ mod tests {
method: Some("GET".to_string()), method: Some("GET".to_string()),
command: None, command: None,
exec_policy_hint: None, exec_policy_hint: None,
attempt_id: None,
}); });
let decision = evaluate_host_policy(&state, Some(&decider), &request) let decision = evaluate_host_policy(&state, Some(&decider), &request)
@@ -336,4 +347,16 @@ mod tests {
); );
assert_eq!(calls.load(Ordering::SeqCst), 0); assert_eq!(calls.load(Ordering::SeqCst), 0);
} }
#[test]
fn ask_uses_decider_source_and_ask_decision() {
assert_eq!(
NetworkDecision::ask(REASON_NOT_ALLOWED),
NetworkDecision::Deny {
reason: REASON_NOT_ALLOWED.to_string(),
source: NetworkDecisionSource::Decider,
decision: NetworkPolicyDecision::Ask,
}
);
}
} }

View File

@@ -1,7 +1,9 @@
use crate::admin; use crate::admin;
use crate::config; use crate::config;
use crate::http_proxy; use crate::http_proxy;
use crate::metadata::proxy_username_for_attempt_id;
use crate::network_policy::NetworkPolicyDecider; use crate::network_policy::NetworkPolicyDecider;
use crate::runtime::BlockedRequest;
use crate::runtime::unix_socket_permissions_supported; use crate::runtime::unix_socket_permissions_supported;
use crate::socks5; use crate::socks5;
use crate::state::NetworkProxyState; use crate::state::NetworkProxyState;
@@ -312,8 +314,12 @@ fn apply_proxy_env_overrides(
socks_addr: SocketAddr, socks_addr: SocketAddr,
socks_enabled: bool, socks_enabled: bool,
allow_local_binding: bool, allow_local_binding: bool,
network_attempt_id: Option<&str>,
) { ) {
let http_proxy_url = format!("http://{http_addr}"); let http_proxy_url = network_attempt_id
.map(proxy_username_for_attempt_id)
.map(|username| format!("http://{username}@{http_addr}"))
.unwrap_or_else(|| format!("http://{http_addr}"));
let socks_proxy_url = format!("socks5h://{socks_addr}"); let socks_proxy_url = format!("socks5h://{socks_addr}");
env.insert( env.insert(
ALLOW_LOCAL_BINDING_ENV_KEY.to_string(), ALLOW_LOCAL_BINDING_ENV_KEY.to_string(),
@@ -354,18 +360,25 @@ fn apply_proxy_env_overrides(
env.insert("ELECTRON_GET_USE_PROXY".to_string(), "true".to_string()); env.insert("ELECTRON_GET_USE_PROXY".to_string(), "true".to_string());
if socks_enabled { // Keep HTTP_PROXY/HTTPS_PROXY as HTTP endpoints. A lot of clients break if
// those vars contain SOCKS URLs. We only switch ALL_PROXY here.
//
// For attempt-scoped runs, point ALL_PROXY at the HTTP proxy URL so the
// attempt metadata survives in proxy credentials for correlation.
if socks_enabled && network_attempt_id.is_none() {
set_env_keys(env, ALL_PROXY_ENV_KEYS, &socks_proxy_url); set_env_keys(env, ALL_PROXY_ENV_KEYS, &socks_proxy_url);
set_env_keys(env, FTP_PROXY_ENV_KEYS, &socks_proxy_url); set_env_keys(env, FTP_PROXY_ENV_KEYS, &socks_proxy_url);
#[cfg(target_os = "macos")]
{
// Preserve existing SSH wrappers (for example: Secretive/Teleport setups)
// and only provide a SOCKS ProxyCommand fallback when one is not present.
env.entry("GIT_SSH_COMMAND".to_string())
.or_insert_with(|| format!("ssh -o ProxyCommand='nc -X 5 -x {socks_addr} %h %p'"));
}
} else { } else {
set_env_keys(env, ALL_PROXY_ENV_KEYS, &http_proxy_url); set_env_keys(env, ALL_PROXY_ENV_KEYS, &http_proxy_url);
set_env_keys(env, FTP_PROXY_ENV_KEYS, &http_proxy_url);
}
#[cfg(target_os = "macos")]
if socks_enabled {
// Preserve existing SSH wrappers (for example: Secretive/Teleport setups)
// and only provide a SOCKS ProxyCommand fallback when one is not present.
env.entry("GIT_SSH_COMMAND".to_string())
.or_insert_with(|| format!("ssh -o ProxyCommand='nc -X 5 -x {socks_addr} %h %p'"));
} }
} }
@@ -386,7 +399,22 @@ impl NetworkProxy {
self.admin_addr self.admin_addr
} }
pub async fn latest_blocked_request_for_attempt(
&self,
attempt_id: &str,
) -> Result<Option<BlockedRequest>> {
self.state.latest_blocked_for_attempt(attempt_id).await
}
pub fn apply_to_env(&self, env: &mut HashMap<String, String>) { pub fn apply_to_env(&self, env: &mut HashMap<String, String>) {
self.apply_to_env_for_attempt(env, None);
}
pub fn apply_to_env_for_attempt(
&self,
env: &mut HashMap<String, String>,
network_attempt_id: Option<&str>,
) {
// Enforce proxying for child processes. We intentionally override existing values so // Enforce proxying for child processes. We intentionally override existing values so
// command-level environment cannot bypass the managed proxy endpoint. // command-level environment cannot bypass the managed proxy endpoint.
apply_proxy_env_overrides( apply_proxy_env_overrides(
@@ -395,6 +423,7 @@ impl NetworkProxy {
self.socks_addr, self.socks_addr,
self.socks_enabled, self.socks_enabled,
self.allow_local_binding, self.allow_local_binding,
network_attempt_id,
); );
} }
@@ -694,6 +723,7 @@ mod tests {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
true, true,
false, false,
None,
); );
assert_eq!( assert_eq!(
@@ -736,6 +766,7 @@ mod tests {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
false, false,
true, true,
None,
); );
assert_eq!( assert_eq!(
@@ -745,6 +776,39 @@ mod tests {
assert_eq!(env.get(ALLOW_LOCAL_BINDING_ENV_KEY), Some(&"1".to_string())); assert_eq!(env.get(ALLOW_LOCAL_BINDING_ENV_KEY), Some(&"1".to_string()));
} }
#[test]
fn apply_proxy_env_overrides_embeds_attempt_id_in_http_proxy_url() {
let mut env = HashMap::new();
apply_proxy_env_overrides(
&mut env,
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
true,
false,
Some("attempt-123"),
);
assert_eq!(
env.get("HTTP_PROXY"),
Some(&"http://codex-net-attempt-attempt-123@127.0.0.1:3128".to_string())
);
assert_eq!(
env.get("HTTPS_PROXY"),
Some(&"http://codex-net-attempt-attempt-123@127.0.0.1:3128".to_string())
);
assert_eq!(
env.get("ALL_PROXY"),
Some(&"http://codex-net-attempt-attempt-123@127.0.0.1:3128".to_string())
);
#[cfg(target_os = "macos")]
assert_eq!(
env.get("GIT_SSH_COMMAND"),
Some(&"ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:8081 %h %p'".to_string())
);
#[cfg(not(target_os = "macos"))]
assert_eq!(env.get("GIT_SSH_COMMAND"), None);
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[test] #[test]
fn apply_proxy_env_overrides_preserves_existing_git_ssh_command() { fn apply_proxy_env_overrides_preserves_existing_git_ssh_command() {
@@ -759,6 +823,7 @@ mod tests {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
true, true,
false, false,
None,
); );
assert_eq!( assert_eq!(

View File

@@ -11,8 +11,6 @@ use rama_http::StatusCode;
use serde::Serialize; use serde::Serialize;
use tracing::error; use tracing::error;
const NETWORK_POLICY_DECISION_PREFIX: &str = "CODEX_NETWORK_POLICY_DECISION";
pub struct PolicyDecisionDetails<'a> { pub struct PolicyDecisionDetails<'a> {
pub decision: NetworkPolicyDecision, pub decision: NetworkPolicyDecision,
pub reason: &'a str, pub reason: &'a str,
@@ -22,17 +20,6 @@ pub struct PolicyDecisionDetails<'a> {
pub port: u16, 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 { pub fn text_response(status: StatusCode, body: &str) -> Response {
Response::builder() Response::builder()
.status(status) .status(status)
@@ -70,7 +57,9 @@ pub fn blocked_header_value(reason: &str) -> &'static str {
pub fn blocked_message(reason: &str) -> &'static str { pub fn blocked_message(reason: &str) -> &'static str {
match reason { match reason {
REASON_NOT_ALLOWED => "Codex blocked this request: domain not in allowlist.", REASON_NOT_ALLOWED => {
"Codex blocked this request: domain not in allowlist (this is not a denylist block)."
}
REASON_NOT_ALLOWED_LOCAL => { REASON_NOT_ALLOWED_LOCAL => {
"Codex blocked this request: local/private addresses not allowed." "Codex blocked this request: local/private addresses not allowed."
} }
@@ -82,31 +71,9 @@ pub fn blocked_message(reason: &str) -> &'static str {
} }
} }
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 { pub fn blocked_message_with_policy(reason: &str, details: &PolicyDecisionDetails<'_>) -> String {
format!( let _ = (details.reason, details.host);
"{}\n{}", blocked_message(reason).to_string()
policy_decision_prefix(details),
blocked_message(reason)
)
} }
pub fn blocked_text_response_with_policy( pub fn blocked_text_response_with_policy(
@@ -128,7 +95,7 @@ mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test] #[test]
fn policy_decision_prefix_serializes_expected_payload() { fn blocked_message_with_policy_returns_human_message() {
let details = PolicyDecisionDetails { let details = PolicyDecisionDetails {
decision: NetworkPolicyDecision::Ask, decision: NetworkPolicyDecision::Ask,
reason: REASON_NOT_ALLOWED, reason: REASON_NOT_ALLOWED,
@@ -138,29 +105,10 @@ mod tests {
port: 443, 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); let message = blocked_message_with_policy(REASON_NOT_ALLOWED, &details);
assert_eq!( assert_eq!(
message, 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 (this is not a denylist block)."
Codex blocked this request: domain not in allowlist."#
); );
} }
} }

View File

@@ -28,11 +28,13 @@ use time::OffsetDateTime;
use tokio::net::lookup_host; use tokio::net::lookup_host;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tokio::time::timeout; use tokio::time::timeout;
use tracing::debug;
use tracing::info; use tracing::info;
use tracing::warn; use tracing::warn;
const MAX_BLOCKED_EVENTS: usize = 200; const MAX_BLOCKED_EVENTS: usize = 200;
const DNS_LOOKUP_TIMEOUT: Duration = Duration::from_secs(2); const DNS_LOOKUP_TIMEOUT: Duration = Duration::from_secs(2);
const NETWORK_POLICY_VIOLATION_PREFIX: &str = "CODEX_NETWORK_POLICY_VIOLATION";
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HostBlockReason { pub enum HostBlockReason {
@@ -71,6 +73,14 @@ pub struct BlockedRequest {
pub method: Option<String>, pub method: Option<String>,
pub mode: Option<NetworkMode>, pub mode: Option<NetworkMode>,
pub protocol: String, pub protocol: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub attempt_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decision: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub port: Option<u16>,
pub timestamp: i64, pub timestamp: i64,
} }
@@ -81,6 +91,10 @@ pub struct BlockedRequestArgs {
pub method: Option<String>, pub method: Option<String>,
pub mode: Option<NetworkMode>, pub mode: Option<NetworkMode>,
pub protocol: String, pub protocol: String,
pub attempt_id: Option<String>,
pub decision: Option<String>,
pub source: Option<String>,
pub port: Option<u16>,
} }
impl BlockedRequest { impl BlockedRequest {
@@ -92,6 +106,10 @@ impl BlockedRequest {
method, method,
mode, mode,
protocol, protocol,
attempt_id,
decision,
source,
port,
} = args; } = args;
Self { Self {
host, host,
@@ -100,11 +118,28 @@ impl BlockedRequest {
method, method,
mode, mode,
protocol, protocol,
attempt_id,
decision,
source,
port,
timestamp: unix_timestamp(), timestamp: unix_timestamp(),
} }
} }
} }
fn blocked_request_violation_log_line(entry: &BlockedRequest) -> String {
match serde_json::to_string(entry) {
Ok(json) => format!("{NETWORK_POLICY_VIOLATION_PREFIX} {json}"),
Err(err) => {
debug!("failed to serialize blocked request for violation log: {err}");
format!(
"{NETWORK_POLICY_VIOLATION_PREFIX} host={} reason={}",
entry.host, entry.reason
)
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct ConfigState { pub struct ConfigState {
pub config: NetworkProxyConfig, pub config: NetworkProxyConfig,
@@ -112,6 +147,7 @@ pub struct ConfigState {
pub deny_set: GlobSet, pub deny_set: GlobSet,
pub constraints: NetworkProxyConstraints, pub constraints: NetworkProxyConstraints,
pub blocked: VecDeque<BlockedRequest>, pub blocked: VecDeque<BlockedRequest>,
pub blocked_total: u64,
} }
#[async_trait] #[async_trait]
@@ -276,14 +312,59 @@ impl NetworkProxyState {
pub async fn record_blocked(&self, entry: BlockedRequest) -> Result<()> { pub async fn record_blocked(&self, entry: BlockedRequest) -> Result<()> {
self.reload_if_needed().await?; self.reload_if_needed().await?;
let violation_line = blocked_request_violation_log_line(&entry);
let mut guard = self.state.write().await; let mut guard = self.state.write().await;
let host = entry.host.clone();
let reason = entry.reason.clone();
let decision = entry.decision.clone();
let source = entry.source.clone();
let protocol = entry.protocol.clone();
let port = entry.port;
let attempt_id = entry.attempt_id.clone();
guard.blocked.push_back(entry); guard.blocked.push_back(entry);
guard.blocked_total = guard.blocked_total.saturating_add(1);
let total = guard.blocked_total;
while guard.blocked.len() > MAX_BLOCKED_EVENTS { while guard.blocked.len() > MAX_BLOCKED_EVENTS {
guard.blocked.pop_front(); guard.blocked.pop_front();
} }
debug!(
"recorded blocked request telemetry (total={}, host={}, reason={}, decision={:?}, source={:?}, protocol={}, port={:?}, attempt_id={:?}, buffered={})",
total,
host,
reason,
decision,
source,
protocol,
port,
attempt_id,
guard.blocked.len()
);
debug!("{violation_line}");
Ok(()) Ok(())
} }
/// Returns a snapshot of buffered blocked-request entries without consuming
/// them.
pub async fn blocked_snapshot(&self) -> Result<Vec<BlockedRequest>> {
self.reload_if_needed().await?;
let guard = self.state.read().await;
Ok(guard.blocked.iter().cloned().collect())
}
pub async fn latest_blocked_for_attempt(
&self,
attempt_id: &str,
) -> Result<Option<BlockedRequest>> {
self.reload_if_needed().await?;
let guard = self.state.read().await;
Ok(guard
.blocked
.iter()
.rev()
.find(|entry| entry.attempt_id.as_deref() == Some(attempt_id))
.cloned())
}
/// Drain and return the buffered blocked-request entries in FIFO order. /// Drain and return the buffered blocked-request entries in FIFO order.
pub async fn drain_blocked(&self) -> Result<Vec<BlockedRequest>> { pub async fn drain_blocked(&self) -> Result<Vec<BlockedRequest>> {
self.reload_if_needed().await?; self.reload_if_needed().await?;
@@ -380,12 +461,17 @@ impl NetworkProxyState {
match self.reloader.maybe_reload().await? { match self.reloader.maybe_reload().await? {
None => Ok(()), None => Ok(()),
Some(mut new_state) => { Some(mut new_state) => {
let (previous_cfg, blocked) = { let (previous_cfg, blocked, blocked_total) = {
let guard = self.state.read().await; let guard = self.state.read().await;
(guard.config.clone(), guard.blocked.clone()) (
guard.config.clone(),
guard.blocked.clone(),
guard.blocked_total,
)
}; };
log_policy_changes(&previous_cfg, &new_state.config); log_policy_changes(&previous_cfg, &new_state.config);
new_state.blocked = blocked; new_state.blocked = blocked;
new_state.blocked_total = blocked_total;
{ {
let mut guard = self.state.write().await; let mut guard = self.state.write().await;
*guard = new_state; *guard = new_state;
@@ -566,6 +652,153 @@ mod tests {
); );
} }
#[tokio::test]
async fn blocked_snapshot_does_not_consume_entries() {
let state = network_proxy_state_for_policy(NetworkProxySettings::default());
state
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
host: "google.com".to_string(),
reason: "not_allowed".to_string(),
client: None,
method: Some("GET".to_string()),
mode: None,
protocol: "http".to_string(),
attempt_id: None,
decision: Some("ask".to_string()),
source: Some("decider".to_string()),
port: Some(80),
}))
.await
.expect("entry should be recorded");
let snapshot = state
.blocked_snapshot()
.await
.expect("snapshot should succeed");
assert_eq!(snapshot.len(), 1);
assert_eq!(snapshot[0].host, "google.com");
assert_eq!(snapshot[0].decision.as_deref(), Some("ask"));
let drained = state
.drain_blocked()
.await
.expect("drain should include snapshot entry");
assert_eq!(drained.len(), 1);
assert_eq!(drained[0].host, snapshot[0].host);
assert_eq!(drained[0].reason, snapshot[0].reason);
assert_eq!(drained[0].decision, snapshot[0].decision);
assert_eq!(drained[0].source, snapshot[0].source);
assert_eq!(drained[0].port, snapshot[0].port);
}
#[tokio::test]
async fn latest_blocked_for_attempt_returns_latest_matching_entry() {
let state = network_proxy_state_for_policy(NetworkProxySettings::default());
state
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
host: "one.example.com".to_string(),
reason: "not_allowed".to_string(),
client: None,
method: Some("GET".to_string()),
mode: None,
protocol: "http".to_string(),
attempt_id: Some("attempt-1".to_string()),
decision: Some("ask".to_string()),
source: Some("decider".to_string()),
port: Some(80),
}))
.await
.expect("entry should be recorded");
state
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
host: "two.example.com".to_string(),
reason: "not_allowed".to_string(),
client: None,
method: Some("GET".to_string()),
mode: None,
protocol: "http".to_string(),
attempt_id: Some("attempt-2".to_string()),
decision: Some("ask".to_string()),
source: Some("decider".to_string()),
port: Some(80),
}))
.await
.expect("entry should be recorded");
state
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
host: "three.example.com".to_string(),
reason: "not_allowed".to_string(),
client: None,
method: Some("GET".to_string()),
mode: None,
protocol: "http".to_string(),
attempt_id: Some("attempt-1".to_string()),
decision: Some("ask".to_string()),
source: Some("decider".to_string()),
port: Some(80),
}))
.await
.expect("entry should be recorded");
let latest = state
.latest_blocked_for_attempt("attempt-1")
.await
.expect("lookup should succeed")
.expect("attempt should have a blocked entry");
assert_eq!(latest.host, "three.example.com");
}
#[tokio::test]
async fn drain_blocked_returns_buffered_window() {
let state = network_proxy_state_for_policy(NetworkProxySettings::default());
for idx in 0..(MAX_BLOCKED_EVENTS + 5) {
state
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
host: format!("example{idx}.com"),
reason: "not_allowed".to_string(),
client: None,
method: Some("GET".to_string()),
mode: None,
protocol: "http".to_string(),
attempt_id: None,
decision: Some("ask".to_string()),
source: Some("decider".to_string()),
port: Some(80),
}))
.await
.expect("entry should be recorded");
}
let blocked = state.drain_blocked().await.expect("drain should succeed");
assert_eq!(blocked.len(), MAX_BLOCKED_EVENTS);
assert_eq!(blocked[0].host, "example5.com");
}
#[test]
fn blocked_request_violation_log_line_serializes_payload() {
let entry = BlockedRequest {
host: "google.com".to_string(),
reason: "not_allowed".to_string(),
client: Some("127.0.0.1".to_string()),
method: Some("GET".to_string()),
mode: Some(NetworkMode::Full),
protocol: "http".to_string(),
attempt_id: Some("attempt-1".to_string()),
decision: Some("ask".to_string()),
source: Some("decider".to_string()),
port: Some(80),
timestamp: 1_735_689_600,
};
assert_eq!(
blocked_request_violation_log_line(&entry),
r#"CODEX_NETWORK_POLICY_VIOLATION {"host":"google.com","reason":"not_allowed","client":"127.0.0.1","method":"GET","mode":"full","protocol":"http","attempt_id":"attempt-1","decision":"ask","source":"decider","port":80,"timestamp":1735689600}"#
);
}
#[tokio::test] #[tokio::test]
async fn host_blocked_subdomain_wildcards_exclude_apex() { async fn host_blocked_subdomain_wildcards_exclude_apex() {
let state = network_proxy_state_for_policy(NetworkProxySettings { let state = network_proxy_state_for_policy(NetworkProxySettings {

View File

@@ -168,6 +168,10 @@ async fn handle_socks5_tcp(
method: None, method: None,
mode: None, mode: None,
protocol: "socks5".to_string(), protocol: "socks5".to_string(),
attempt_id: None,
decision: Some(details.decision.as_str().to_string()),
source: Some(details.source.as_str().to_string()),
port: Some(port),
})) }))
.await; .await;
let client = client.as_deref().unwrap_or_default(); let client = client.as_deref().unwrap_or_default();
@@ -198,6 +202,10 @@ async fn handle_socks5_tcp(
method: None, method: None,
mode: Some(NetworkMode::Limited), mode: Some(NetworkMode::Limited),
protocol: "socks5".to_string(), protocol: "socks5".to_string(),
attempt_id: None,
decision: Some(details.decision.as_str().to_string()),
source: Some(details.source.as_str().to_string()),
port: Some(port),
})) }))
.await; .await;
let client = client.as_deref().unwrap_or_default(); let client = client.as_deref().unwrap_or_default();
@@ -221,6 +229,7 @@ async fn handle_socks5_tcp(
method: None, method: None,
command: None, command: None,
exec_policy_hint: None, exec_policy_hint: None,
attempt_id: None,
}); });
match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await { match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await {
@@ -245,6 +254,10 @@ async fn handle_socks5_tcp(
method: None, method: None,
mode: None, mode: None,
protocol: "socks5".to_string(), protocol: "socks5".to_string(),
attempt_id: None,
decision: Some(details.decision.as_str().to_string()),
source: Some(details.source.as_str().to_string()),
port: Some(port),
})) }))
.await; .await;
let client = client.as_deref().unwrap_or_default(); let client = client.as_deref().unwrap_or_default();
@@ -305,6 +318,10 @@ async fn inspect_socks5_udp(
method: None, method: None,
mode: None, mode: None,
protocol: "socks5-udp".to_string(), protocol: "socks5-udp".to_string(),
attempt_id: None,
decision: Some(details.decision.as_str().to_string()),
source: Some(details.source.as_str().to_string()),
port: Some(port),
})) }))
.await; .await;
let client = client.as_deref().unwrap_or_default(); let client = client.as_deref().unwrap_or_default();
@@ -335,6 +352,10 @@ async fn inspect_socks5_udp(
method: None, method: None,
mode: Some(NetworkMode::Limited), mode: Some(NetworkMode::Limited),
protocol: "socks5-udp".to_string(), protocol: "socks5-udp".to_string(),
attempt_id: None,
decision: Some(details.decision.as_str().to_string()),
source: Some(details.source.as_str().to_string()),
port: Some(port),
})) }))
.await; .await;
return Err(policy_denied_error(REASON_METHOD_NOT_ALLOWED, &details)); return Err(policy_denied_error(REASON_METHOD_NOT_ALLOWED, &details));
@@ -354,6 +375,7 @@ async fn inspect_socks5_udp(
method: None, method: None,
command: None, command: None,
exec_policy_hint: None, exec_policy_hint: None,
attempt_id: None,
}); });
match evaluate_host_policy(&state, policy_decider.as_ref(), &request).await { match evaluate_host_policy(&state, policy_decider.as_ref(), &request).await {
@@ -378,6 +400,10 @@ async fn inspect_socks5_udp(
method: None, method: None,
mode: None, mode: None,
protocol: "socks5-udp".to_string(), protocol: "socks5-udp".to_string(),
attempt_id: None,
decision: Some(details.decision.as_str().to_string()),
source: Some(details.source.as_str().to_string()),
port: Some(port),
})) }))
.await; .await;
let client = client.as_deref().unwrap_or_default(); let client = client.as_deref().unwrap_or_default();

View File

@@ -60,6 +60,7 @@ pub fn build_config_state(
deny_set, deny_set,
constraints, constraints,
blocked: std::collections::VecDeque::new(), blocked: std::collections::VecDeque::new(),
blocked_total: 0,
}) })
} }