mirror of
https://github.com/openai/codex.git
synced 2026-03-05 21:45:28 +03:00
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:
@@ -1,3 +1,5 @@
|
|||||||
iTerm
|
iTerm
|
||||||
iTerm2
|
iTerm2
|
||||||
psuedo
|
psuedo
|
||||||
|
te
|
||||||
|
TE
|
||||||
|
|||||||
@@ -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
1
codex-rs/Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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}");
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
50
codex-rs/network-proxy/src/metadata.rs
Normal file
50
codex-rs/network-proxy/src/metadata.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
@@ -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."#
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user