mirror of
https://github.com/openai/codex.git
synced 2026-10-26 18:38:47 +00:00
Merge dev/winston/credentialed-routes-discovery into dev/winston/credentialed-routes-mitm
This commit is contained in:
@@ -16,6 +16,7 @@ use crate::upstream::UpstreamClient;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
|
||||
use rama_core::Layer;
|
||||
use rama_core::Service;
|
||||
use rama_core::bytes::Bytes;
|
||||
@@ -103,6 +104,8 @@ impl std::fmt::Debug for MitmState {
|
||||
|
||||
impl MitmState {
|
||||
pub(crate) fn new(config: MitmUpstreamConfig) -> Result<Self> {
|
||||
ensure_rustls_crypto_provider();
|
||||
|
||||
// MITM exists when HTTPS policy depends on the inner request: limited-mode method clamps
|
||||
// and host-specific hooks both need visibility after CONNECT is established. We
|
||||
// generate/load a local CA and issue per-host leaf certs so we can terminate TLS and
|
||||
@@ -358,16 +361,12 @@ async fn evaluate_mitm_policy(
|
||||
return Ok(MitmPolicyDecision::Block(blocked_text_response(reason)));
|
||||
}
|
||||
|
||||
match policy
|
||||
let hook_actions = match policy
|
||||
.app_state
|
||||
.evaluate_mitm_hook_request(&policy.target_host, req)
|
||||
.await?
|
||||
{
|
||||
HookEvaluation::Matched { actions } => {
|
||||
return Ok(MitmPolicyDecision::Allow {
|
||||
hook_actions: Some(actions),
|
||||
});
|
||||
}
|
||||
HookEvaluation::Matched { actions } => Some(actions),
|
||||
HookEvaluation::HookedHostNoMatch => {
|
||||
let _ = policy
|
||||
.app_state
|
||||
@@ -391,8 +390,8 @@ async fn evaluate_mitm_policy(
|
||||
REASON_MITM_HOOK_DENIED,
|
||||
)));
|
||||
}
|
||||
HookEvaluation::NoHooksForHost => {}
|
||||
}
|
||||
HookEvaluation::NoHooksForHost => None,
|
||||
};
|
||||
|
||||
if !policy.mode.allows_method(&method) {
|
||||
let _ = policy
|
||||
@@ -418,7 +417,7 @@ async fn evaluate_mitm_policy(
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(MitmPolicyDecision::Allow { hook_actions: None })
|
||||
Ok(MitmPolicyDecision::Allow { hook_actions })
|
||||
}
|
||||
|
||||
fn apply_mitm_hook_actions(headers: &mut HeaderMap, actions: Option<&MitmHookActions>) {
|
||||
|
||||
@@ -192,6 +192,50 @@ async fn mitm_policy_allows_matching_hooked_write_in_full_mode() {
|
||||
assert_eq!(app_state.blocked_snapshot().await.unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mitm_policy_blocks_matching_hooked_write_in_limited_mode() {
|
||||
let mut hook = github_write_hook();
|
||||
hook.actions.inject_request_headers.clear();
|
||||
let mut network = NetworkProxySettings {
|
||||
mitm: true,
|
||||
mitm_hooks: vec![hook],
|
||||
mode: NetworkMode::Limited,
|
||||
..NetworkProxySettings::default()
|
||||
};
|
||||
network.set_allowed_domains(vec!["api.github.com".to_string()]);
|
||||
let app_state = Arc::new(network_proxy_state_for_policy(network));
|
||||
let ctx = policy_ctx(
|
||||
app_state.clone(),
|
||||
NetworkMode::Limited,
|
||||
"api.github.com",
|
||||
/*target_port*/ 443,
|
||||
);
|
||||
let req = Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/repos/openai/codex/issues")
|
||||
.header(HOST, "api.github.com")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response = mitm_blocking_response(&req, &ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("matching POST hook should still be blocked in limited mode");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
assert_eq!(
|
||||
response.headers().get("x-proxy-error").unwrap(),
|
||||
"blocked-by-method-policy"
|
||||
);
|
||||
|
||||
let blocked = app_state.drain_blocked().await.unwrap();
|
||||
assert_eq!(blocked.len(), 1);
|
||||
assert_eq!(blocked[0].reason, REASON_METHOD_NOT_ALLOWED);
|
||||
assert_eq!(blocked[0].method.as_deref(), Some("POST"));
|
||||
assert_eq!(blocked[0].host, "api.github.com");
|
||||
assert_eq!(blocked[0].port, Some(443));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mitm_policy_blocks_hook_miss_for_hooked_host_and_records_telemetry_in_full_mode() {
|
||||
let secret_file = NamedTempFile::new().unwrap();
|
||||
|
||||
@@ -375,12 +375,14 @@ pub const ALL_PROXY_ENV_KEYS: &[&str] = &["ALL_PROXY", "all_proxy"];
|
||||
pub const PROXY_ACTIVE_ENV_KEY: &str = "CODEX_NETWORK_PROXY_ACTIVE";
|
||||
pub const ALLOW_LOCAL_BINDING_ENV_KEY: &str = "CODEX_NETWORK_ALLOW_LOCAL_BINDING";
|
||||
const ELECTRON_GET_USE_PROXY_ENV_KEY: &str = "ELECTRON_GET_USE_PROXY";
|
||||
const NODE_USE_ENV_PROXY_ENV_KEY: &str = "NODE_USE_ENV_PROXY";
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
const GIT_SSH_COMMAND_ENV_KEY: &str = "GIT_SSH_COMMAND";
|
||||
pub const PROXY_ENV_KEYS: &[&str] = &[
|
||||
PROXY_ACTIVE_ENV_KEY,
|
||||
ALLOW_LOCAL_BINDING_ENV_KEY,
|
||||
ELECTRON_GET_USE_PROXY_ENV_KEY,
|
||||
NODE_USE_ENV_PROXY_ENV_KEY,
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"http_proxy",
|
||||
@@ -545,6 +547,8 @@ fn apply_proxy_env_overrides(
|
||||
ELECTRON_GET_USE_PROXY_ENV_KEY.to_string(),
|
||||
"true".to_string(),
|
||||
);
|
||||
// Node.js built-in HTTP clients only honor proxy environment variables when this is enabled.
|
||||
env.insert(NODE_USE_ENV_PROXY_ENV_KEY.to_string(), "1".to_string());
|
||||
|
||||
// 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.
|
||||
@@ -1058,6 +1062,7 @@ mod tests {
|
||||
env.get(ELECTRON_GET_USE_PROXY_ENV_KEY),
|
||||
Some(&"true".to_string())
|
||||
);
|
||||
assert_eq!(env.get(NODE_USE_ENV_PROXY_ENV_KEY), Some(&"1".to_string()));
|
||||
#[cfg(target_os = "macos")]
|
||||
assert_eq!(
|
||||
env.get(GIT_SSH_COMMAND_ENV_KEY),
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::network_policy::emit_block_decision_audit_event;
|
||||
use crate::network_policy::evaluate_host_policy;
|
||||
use crate::policy::normalize_host;
|
||||
use crate::reasons::REASON_METHOD_NOT_ALLOWED;
|
||||
use crate::reasons::REASON_MITM_REQUIRED;
|
||||
use crate::reasons::REASON_PROXY_DISABLED;
|
||||
use crate::responses::PolicyDecisionDetails;
|
||||
use crate::responses::blocked_message_with_policy;
|
||||
@@ -240,6 +241,51 @@ async fn handle_socks5_tcp(
|
||||
}
|
||||
}
|
||||
|
||||
match app_state.host_has_mitm_hooks(&host).await {
|
||||
Ok(true) => {
|
||||
emit_socks_block_decision_audit_event(
|
||||
&app_state,
|
||||
NetworkDecisionSource::ModeGuard,
|
||||
REASON_MITM_REQUIRED,
|
||||
NetworkProtocol::Socks5Tcp,
|
||||
host.as_str(),
|
||||
port,
|
||||
client.as_deref(),
|
||||
);
|
||||
let details = PolicyDecisionDetails {
|
||||
decision: NetworkPolicyDecision::Deny,
|
||||
reason: REASON_MITM_REQUIRED,
|
||||
source: NetworkDecisionSource::ModeGuard,
|
||||
protocol: NetworkProtocol::Socks5Tcp,
|
||||
host: &host,
|
||||
port,
|
||||
};
|
||||
let _ = app_state
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: REASON_MITM_REQUIRED.to_string(),
|
||||
client: client.clone(),
|
||||
method: None,
|
||||
mode: Some(NetworkMode::Full),
|
||||
protocol: "socks5".to_string(),
|
||||
decision: Some(details.decision.as_str().to_string()),
|
||||
source: Some(details.source.as_str().to_string()),
|
||||
port: Some(port),
|
||||
}))
|
||||
.await;
|
||||
let client = client.as_deref().unwrap_or_default();
|
||||
warn!(
|
||||
"SOCKS blocked; MITM required to enforce HTTPS policy (client={client}, host={host}, mode=full)"
|
||||
);
|
||||
return Err(policy_denied_error(REASON_MITM_REQUIRED, &details).into());
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(err) => {
|
||||
error!("failed to inspect MITM hooks for {host}: {err}");
|
||||
return Err(io::Error::other("proxy error").into());
|
||||
}
|
||||
}
|
||||
|
||||
let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs {
|
||||
protocol: NetworkProtocol::Socks5Tcp,
|
||||
host: host.clone(),
|
||||
@@ -501,11 +547,14 @@ mod tests {
|
||||
use crate::config::NetworkMode;
|
||||
use crate::config::NetworkProxyConfig;
|
||||
use crate::config::NetworkProxySettings;
|
||||
use crate::mitm_hook::MitmHookConfig;
|
||||
use crate::mitm_hook::MitmHookMatchConfig;
|
||||
use crate::network_policy::test_support::POLICY_DECISION_EVENT_NAME;
|
||||
use crate::network_policy::test_support::capture_events;
|
||||
use crate::network_policy::test_support::find_event_by_name;
|
||||
use crate::runtime::ConfigReloader;
|
||||
use crate::runtime::ConfigState;
|
||||
use crate::runtime::network_proxy_state_for_policy;
|
||||
use crate::state::NetworkProxyConstraints;
|
||||
use crate::state::build_config_state;
|
||||
use async_trait::async_trait;
|
||||
@@ -589,6 +638,64 @@ mod tests {
|
||||
assert_eq!(event.field("client.address"), Some("unknown"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn handle_socks5_tcp_blocks_hooked_host_in_full_mode() {
|
||||
let state = Arc::new(network_proxy_state_for_policy(NetworkProxySettings {
|
||||
enabled: true,
|
||||
mode: NetworkMode::Full,
|
||||
mitm: true,
|
||||
mitm_hooks: vec![MitmHookConfig {
|
||||
host: "api.github.com".to_string(),
|
||||
matcher: MitmHookMatchConfig {
|
||||
methods: vec!["GET".to_string()],
|
||||
path_prefixes: vec!["/".to_string()],
|
||||
..MitmHookMatchConfig::default()
|
||||
},
|
||||
..MitmHookConfig::default()
|
||||
}],
|
||||
..NetworkProxySettings::default()
|
||||
}));
|
||||
let mut request =
|
||||
TcpRequest::new(HostWithPort::try_from("api.github.com:443").expect("valid authority"));
|
||||
request.extensions_mut().insert(state.clone());
|
||||
|
||||
let (result, events) = capture_events(|| async {
|
||||
handle_socks5_tcp(
|
||||
request,
|
||||
TargetCheckedTcpConnector::new(state.clone()),
|
||||
/*policy_decider*/ None,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
assert!(result.is_err(), "hooked host should require MITM");
|
||||
|
||||
let blocked = state.drain_blocked().await.unwrap();
|
||||
assert_eq!(blocked.len(), 1);
|
||||
assert_eq!(blocked[0].reason, REASON_MITM_REQUIRED);
|
||||
assert_eq!(blocked[0].host, "api.github.com");
|
||||
assert_eq!(blocked[0].port, Some(443));
|
||||
assert_eq!(blocked[0].protocol, "socks5");
|
||||
|
||||
let event = find_event_by_name(&events, POLICY_DECISION_EVENT_NAME)
|
||||
.expect("expected policy decision event");
|
||||
assert_eq!(event.field("network.policy.scope"), Some("non_domain"));
|
||||
assert_eq!(event.field("network.policy.decision"), Some("deny"));
|
||||
assert_eq!(event.field("network.policy.source"), Some("mode_guard"));
|
||||
assert_eq!(
|
||||
event.field("network.policy.reason"),
|
||||
Some(REASON_MITM_REQUIRED)
|
||||
);
|
||||
assert_eq!(
|
||||
event.field("network.transport.protocol"),
|
||||
Some("socks5_tcp")
|
||||
);
|
||||
assert_eq!(event.field("server.address"), Some("api.github.com"));
|
||||
assert_eq!(event.field("server.port"), Some("443"));
|
||||
assert_eq!(event.field("http.request.method"), Some("none"));
|
||||
assert_eq!(event.field("client.address"), Some("unknown"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn inspect_socks5_udp_emits_block_decision_for_mode_guard_deny() {
|
||||
let state = state_for_settings(NetworkProxySettings {
|
||||
|
||||
Reference in New Issue
Block a user