From 2c9ccbe03076dcbefa986049b734d117ab3a9fae Mon Sep 17 00:00:00 2001 From: Eva Wong Date: Fri, 1 May 2026 11:17:14 -0700 Subject: [PATCH] Wire MITM hooks into runtime enforcement --- codex-rs/network-proxy/src/http_proxy.rs | 66 ++++++++-- codex-rs/network-proxy/src/mitm.rs | 98 +++++++++++++-- codex-rs/network-proxy/src/mitm_tests.rs | 148 +++++++++++++++++++++++ codex-rs/network-proxy/src/reasons.rs | 1 + codex-rs/network-proxy/src/responses.rs | 3 + codex-rs/network-proxy/src/runtime.rs | 38 +++++- codex-rs/network-proxy/src/state.rs | 3 + 7 files changed, 337 insertions(+), 20 deletions(-) diff --git a/codex-rs/network-proxy/src/http_proxy.rs b/codex-rs/network-proxy/src/http_proxy.rs index fd42fc92e0..f20c01b906 100644 --- a/codex-rs/network-proxy/src/http_proxy.rs +++ b/codex-rs/network-proxy/src/http_proxy.rs @@ -80,6 +80,9 @@ use tracing::error; use tracing::info; use tracing::warn; +#[derive(Clone, Copy, Debug)] +struct ConnectMitmEnabled(bool); + pub async fn run_http_proxy( state: Arc, addr: SocketAddr, @@ -256,10 +259,18 @@ async fn http_connect_accept( return Err(text_response(StatusCode::INTERNAL_SERVER_ERROR, "error")); } }; + let host_has_mitm_hooks = match app_state.host_has_mitm_hooks(&host).await { + Ok(has_hooks) => has_hooks, + Err(err) => { + error!("failed to inspect MITM hooks for {host}: {err}"); + return Err(text_response(StatusCode::INTERNAL_SERVER_ERROR, "error")); + } + }; + let connect_needs_mitm = mode == NetworkMode::Limited || host_has_mitm_hooks; - if mode == NetworkMode::Limited && mitm_state.is_none() { - // Limited mode is designed to be read-only. Without MITM, a CONNECT tunnel would hide the - // inner HTTP method/headers from the proxy, effectively bypassing method policy. + if connect_needs_mitm && mitm_state.is_none() { + // CONNECT needs MITM whenever HTTPS policy depends on inner-request inspection, either for + // limited-mode method enforcement or for host-specific MITM hooks. emit_http_block_decision_audit_event( &app_state, BlockDecisionAuditEventArgs { @@ -286,7 +297,7 @@ async fn http_connect_accept( reason: REASON_MITM_REQUIRED.to_string(), client: client.clone(), method: Some("CONNECT".to_string()), - mode: Some(NetworkMode::Limited), + mode: Some(mode), protocol: "http-connect".to_string(), decision: Some(details.decision.as_str().to_string()), source: Some(details.source.as_str().to_string()), @@ -295,14 +306,16 @@ async fn http_connect_accept( .await; let client = client.as_deref().unwrap_or_default(); warn!( - "CONNECT blocked; MITM required for read-only HTTPS in limited mode (client={client}, host={host}, mode=limited, allowed_methods=GET, HEAD, OPTIONS)" + "CONNECT blocked; MITM required to enforce HTTPS policy (client={client}, host={host}, mode={mode:?}, hooked_host={host_has_mitm_hooks})" ); return Err(blocked_text_with_details(REASON_MITM_REQUIRED, &details)); } req.extensions_mut().insert(ProxyTarget(authority)); + req.extensions_mut() + .insert(ConnectMitmEnabled(connect_needs_mitm)); req.extensions_mut().insert(mode); - if let Some(mitm_state) = mitm_state { + if connect_needs_mitm && let Some(mitm_state) = mitm_state { req.extensions_mut().insert(mitm_state); } @@ -331,7 +344,10 @@ async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> { return Ok(()); }; - if mode == NetworkMode::Limited + if upgraded + .extensions() + .get::() + .is_some_and(|enabled| enabled.0) && upgraded .extensions() .get::>() @@ -1094,6 +1110,42 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); } + #[tokio::test] + async fn http_connect_accept_blocks_hooked_host_in_full_mode_without_mitm_state() { + let mut policy = NetworkProxySettings { + mitm: true, + mitm_hooks: vec![crate::mitm_hook::MitmHookConfig { + host: "api.github.com".to_string(), + matcher: crate::mitm_hook::MitmHookMatchConfig { + methods: vec!["POST".to_string()], + path_prefixes: vec!["/repos/openai/".to_string()], + ..crate::mitm_hook::MitmHookMatchConfig::default() + }, + actions: crate::mitm_hook::MitmHookActionsConfig::default(), + }], + ..Default::default() + }; + policy.set_allowed_domains(vec!["api.github.com".to_string()]); + let state = Arc::new(network_proxy_state_for_policy(policy)); + + let mut req = Request::builder() + .method(Method::CONNECT) + .uri("https://api.github.com:443") + .header("host", "api.github.com:443") + .body(Body::empty()) + .unwrap(); + req.extensions_mut().insert(state); + + let response = http_connect_accept(/*policy_decider*/ None, req) + .await + .unwrap_err(); + assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!( + response.headers().get("x-proxy-error").unwrap(), + "blocked-by-mitm-required" + ); + } + #[tokio::test] async fn http_proxy_listener_accepts_plain_http1_connect_requests() { let target_listener = TokioTcpListener::bind((Ipv4Addr::LOCALHOST, 0)) diff --git a/codex-rs/network-proxy/src/mitm.rs b/codex-rs/network-proxy/src/mitm.rs index 7be700b1df..b14b39ea5e 100644 --- a/codex-rs/network-proxy/src/mitm.rs +++ b/codex-rs/network-proxy/src/mitm.rs @@ -1,7 +1,10 @@ use crate::certs::ManagedMitmCa; use crate::config::NetworkMode; +use crate::mitm_hook::HookEvaluation; +use crate::mitm_hook::MitmHookActions; use crate::policy::normalize_host; use crate::reasons::REASON_METHOD_NOT_ALLOWED; +use crate::reasons::REASON_MITM_HOOK_DENIED; use crate::responses::blocked_text_response; use crate::responses::text_response; use crate::runtime::HostBlockDecision; @@ -23,6 +26,7 @@ use rama_core::rt::Executor; use rama_core::service::service_fn; use rama_http::Body; use rama_http::BodyDataStream; +use rama_http::HeaderMap; use rama_http::HeaderValue; use rama_http::Request; use rama_http::Response; @@ -71,6 +75,13 @@ struct MitmRequestContext { mitm: Arc, } +enum MitmPolicyDecision { + Allow { + hook_actions: Option, + }, + Block(Response), +} + const MITM_INSPECT_BODIES: bool = false; const MITM_MAX_BODY_BYTES: usize = 4096; @@ -86,9 +97,10 @@ impl std::fmt::Debug for MitmState { impl MitmState { pub(crate) fn new(config: MitmUpstreamConfig) -> Result { - // MITM exists to make limited-mode HTTPS enforceable: once CONNECT is established, plain - // proxying would lose visibility into the inner HTTP request. We generate/load a local CA - // and issue per-host leaf certs so we can terminate TLS and apply policy. + // 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 + // apply policy. let ca = ManagedMitmCa::load_or_create()?; let upstream = if config.allow_upstream_proxy { @@ -200,9 +212,10 @@ async fn handle_mitm_request( } async fn forward_request(req: Request, request_ctx: &MitmRequestContext) -> Result { - if let Some(response) = mitm_blocking_response(&req, &request_ctx.policy).await? { - return Ok(response); - } + let hook_actions = match evaluate_mitm_policy(&req, &request_ctx.policy).await? { + MitmPolicyDecision::Allow { hook_actions } => hook_actions, + MitmPolicyDecision::Block(response) => return Ok(response), + }; let target_host = request_ctx.policy.target_host.clone(); let target_port = request_ctx.policy.target_port; @@ -213,6 +226,7 @@ async fn forward_request(req: Request, request_ctx: &MitmRequestContext) -> Resu let log_path = path_for_log(req.uri()); let (mut parts, body) = req.into_parts(); + apply_mitm_hook_actions(&mut parts.headers, hook_actions.as_ref()); let authority = authority_header_value(&target_host, target_port); parts.uri = build_https_uri(&authority, &path)?; parts @@ -247,12 +261,23 @@ async fn forward_request(req: Request, request_ctx: &MitmRequestContext) -> Resu ) } +#[cfg_attr(not(test), allow(dead_code))] async fn mitm_blocking_response( req: &Request, policy: &MitmPolicyContext, ) -> Result> { + match evaluate_mitm_policy(req, policy).await? { + MitmPolicyDecision::Allow { .. } => Ok(None), + MitmPolicyDecision::Block(response) => Ok(Some(response)), + } +} + +async fn evaluate_mitm_policy( + req: &Request, + policy: &MitmPolicyContext, +) -> Result { if req.method().as_str() == "CONNECT" { - return Ok(Some(text_response( + return Ok(MitmPolicyDecision::Block(text_response( StatusCode::METHOD_NOT_ALLOWED, "CONNECT not supported inside MITM", ))); @@ -272,7 +297,7 @@ async fn mitm_blocking_response( "MITM host mismatch (target={}, request_host={normalized})", policy.target_host ); - return Ok(Some(text_response( + return Ok(MitmPolicyDecision::Block(text_response( StatusCode::BAD_REQUEST, "host mismatch", ))); @@ -307,7 +332,43 @@ async fn mitm_blocking_response( "MITM blocked local/private target after CONNECT (host={}, port={}, method={method}, path={log_path})", policy.target_host, policy.target_port ); - return Ok(Some(blocked_text_response(reason))); + return Ok(MitmPolicyDecision::Block(blocked_text_response(reason))); + } + + 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::HookedHostNoMatch => { + let _ = policy + .app_state + .record_blocked(BlockedRequest::new(BlockedRequestArgs { + host: policy.target_host.clone(), + reason: REASON_MITM_HOOK_DENIED.to_string(), + client: client.clone(), + method: Some(method.clone()), + mode: Some(policy.mode), + protocol: "https".to_string(), + decision: None, + source: None, + port: Some(policy.target_port), + })) + .await; + warn!( + "MITM blocked by hook policy (host={}, method={method}, mode={:?})", + policy.target_host, policy.mode + ); + return Ok(MitmPolicyDecision::Block(blocked_text_response( + REASON_MITM_HOOK_DENIED, + ))); + } + HookEvaluation::NoHooksForHost => {} } if !policy.mode.allows_method(&method) { @@ -329,10 +390,25 @@ async fn mitm_blocking_response( "MITM blocked by method policy (host={}, method={method}, path={log_path}, mode={:?}, allowed_methods=GET, HEAD, OPTIONS)", policy.target_host, policy.mode ); - return Ok(Some(blocked_text_response(REASON_METHOD_NOT_ALLOWED))); + return Ok(MitmPolicyDecision::Block(blocked_text_response( + REASON_METHOD_NOT_ALLOWED, + ))); } - Ok(None) + Ok(MitmPolicyDecision::Allow { hook_actions: None }) +} + +fn apply_mitm_hook_actions(headers: &mut HeaderMap, actions: Option<&MitmHookActions>) { + let Some(actions) = actions else { + return; + }; + + for header_name in &actions.strip_request_headers { + headers.remove(header_name); + } + for injected_header in &actions.inject_request_headers { + headers.insert(injected_header.name.clone(), injected_header.value.clone()); + } } fn respond_with_inspection( diff --git a/codex-rs/network-proxy/src/mitm_tests.rs b/codex-rs/network-proxy/src/mitm_tests.rs index 8623720752..7adc97aa4e 100644 --- a/codex-rs/network-proxy/src/mitm_tests.rs +++ b/codex-rs/network-proxy/src/mitm_tests.rs @@ -2,13 +2,39 @@ use super::*; use crate::config::NetworkProxySettings; use crate::reasons::REASON_METHOD_NOT_ALLOWED; +use crate::reasons::REASON_MITM_HOOK_DENIED; use crate::reasons::REASON_NOT_ALLOWED_LOCAL; use crate::runtime::network_proxy_state_for_policy; +use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use rama_http::Body; +use rama_http::HeaderMap; +use rama_http::HeaderValue; use rama_http::Method; use rama_http::Request; use rama_http::StatusCode; +use rama_http::header::HeaderName; +use tempfile::NamedTempFile; + +fn github_write_hook() -> crate::mitm_hook::MitmHookConfig { + crate::mitm_hook::MitmHookConfig { + host: "api.github.com".to_string(), + matcher: crate::mitm_hook::MitmHookMatchConfig { + methods: vec!["POST".to_string(), "PUT".to_string()], + path_prefixes: vec!["/repos/openai/".to_string()], + ..crate::mitm_hook::MitmHookMatchConfig::default() + }, + actions: crate::mitm_hook::MitmHookActionsConfig { + strip_request_headers: vec!["authorization".to_string()], + inject_request_headers: vec![crate::mitm_hook::InjectedHeaderConfig { + name: "authorization".to_string(), + secret_env_var: Some("CODEX_GITHUB_TOKEN".to_string()), + secret_file: None, + prefix: Some("Bearer ".to_string()), + }], + }, + } +} fn policy_ctx( app_state: Arc, @@ -126,3 +152,125 @@ async fn mitm_policy_rechecks_local_private_target_after_connect() { assert_eq!(blocked[0].host, "10.0.0.1"); assert_eq!(blocked[0].port, Some(443)); } + +#[tokio::test] +async fn mitm_policy_allows_matching_hooked_write_in_full_mode() { + let secret_file = NamedTempFile::new().unwrap(); + std::fs::write(secret_file.path(), "ghp-secret\n").unwrap(); + let mut hook = github_write_hook(); + hook.actions.inject_request_headers[0].secret_env_var = None; + hook.actions.inject_request_headers[0].secret_file = + Some(secret_file.path().display().to_string()); + let mut network = NetworkProxySettings { + mitm: true, + mitm_hooks: vec![hook], + mode: NetworkMode::Full, + ..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::Full, + "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(); + + assert!( + response.is_none(), + "matching hook should be allowed in full mode" + ); + assert_eq!(app_state.blocked_snapshot().await.unwrap().len(), 0); +} + +#[tokio::test] +async fn mitm_policy_blocks_hook_miss_for_hooked_host_and_records_telemetry_in_full_mode() { + let secret_file = NamedTempFile::new().unwrap(); + std::fs::write(secret_file.path(), "ghp-secret\n").unwrap(); + let mut hook = github_write_hook(); + hook.actions.inject_request_headers[0].secret_env_var = None; + hook.actions.inject_request_headers[0].secret_file = + Some(secret_file.path().display().to_string()); + let mut network = NetworkProxySettings { + mitm: true, + mitm_hooks: vec![hook], + mode: NetworkMode::Full, + ..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::Full, + "api.github.com", + /*target_port*/ 443, + ); + let req = Request::builder() + .method(Method::GET) + .uri("/repos/openai/codex/issues?token=secret") + .header(HOST, "api.github.com") + .header("authorization", "Bearer user-supplied") + .body(Body::empty()) + .unwrap(); + + let response = mitm_blocking_response(&req, &ctx) + .await + .unwrap() + .expect("hook miss should be blocked"); + + assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!( + response.headers().get("x-proxy-error").unwrap(), + "blocked-by-mitm-hook" + ); + + let blocked = app_state.drain_blocked().await.unwrap(); + assert_eq!(blocked.len(), 1); + assert_eq!(blocked[0].reason, REASON_MITM_HOOK_DENIED); + assert_eq!(blocked[0].method.as_deref(), Some("GET")); + assert_eq!(blocked[0].host, "api.github.com"); + assert_eq!(blocked[0].port, Some(443)); +} + +#[test] +fn apply_mitm_hook_actions_replaces_authorization_header() { + let mut headers = HeaderMap::new(); + headers.append( + HeaderName::from_static("authorization"), + HeaderValue::from_static("Bearer user-supplied"), + ); + headers.append( + HeaderName::from_static("x-request-id"), + HeaderValue::from_static("req_123"), + ); + + let actions = crate::mitm_hook::MitmHookActions { + strip_request_headers: vec![HeaderName::from_static("authorization")], + inject_request_headers: vec![crate::mitm_hook::ResolvedInjectedHeader { + name: HeaderName::from_static("authorization"), + value: HeaderValue::from_static("Bearer secret-token"), + source: crate::mitm_hook::SecretSource::File( + AbsolutePathBuf::try_from("/tmp/github-token").unwrap(), + ), + }], + }; + + apply_mitm_hook_actions(&mut headers, Some(&actions)); + + assert_eq!( + headers.get("authorization"), + Some(&HeaderValue::from_static("Bearer secret-token")) + ); + assert_eq!( + headers.get("x-request-id"), + Some(&HeaderValue::from_static("req_123")) + ); +} diff --git a/codex-rs/network-proxy/src/reasons.rs b/codex-rs/network-proxy/src/reasons.rs index b844eadf3d..67f570f128 100644 --- a/codex-rs/network-proxy/src/reasons.rs +++ b/codex-rs/network-proxy/src/reasons.rs @@ -1,5 +1,6 @@ pub(crate) const REASON_DENIED: &str = "denied"; pub(crate) const REASON_METHOD_NOT_ALLOWED: &str = "method_not_allowed"; +pub(crate) const REASON_MITM_HOOK_DENIED: &str = "mitm_hook_denied"; pub(crate) const REASON_MITM_REQUIRED: &str = "mitm_required"; pub(crate) const REASON_NOT_ALLOWED: &str = "not_allowed"; pub(crate) const REASON_NOT_ALLOWED_LOCAL: &str = "not_allowed_local"; diff --git a/codex-rs/network-proxy/src/responses.rs b/codex-rs/network-proxy/src/responses.rs index c0418c72b5..d2aeb990e8 100644 --- a/codex-rs/network-proxy/src/responses.rs +++ b/codex-rs/network-proxy/src/responses.rs @@ -3,6 +3,7 @@ use crate::network_policy::NetworkPolicyDecision; use crate::network_policy::NetworkProtocol; use crate::reasons::REASON_DENIED; use crate::reasons::REASON_METHOD_NOT_ALLOWED; +use crate::reasons::REASON_MITM_HOOK_DENIED; use crate::reasons::REASON_MITM_REQUIRED; use crate::reasons::REASON_NOT_ALLOWED; use crate::reasons::REASON_NOT_ALLOWED_LOCAL; @@ -53,6 +54,7 @@ pub fn blocked_header_value(reason: &str) -> &'static str { REASON_NOT_ALLOWED | REASON_NOT_ALLOWED_LOCAL => "blocked-by-allowlist", REASON_DENIED => "blocked-by-denylist", REASON_METHOD_NOT_ALLOWED => "blocked-by-method-policy", + REASON_MITM_HOOK_DENIED => "blocked-by-mitm-hook", REASON_MITM_REQUIRED => "blocked-by-mitm-required", _ => "blocked-by-policy", } @@ -64,6 +66,7 @@ pub fn blocked_message(reason: &str) -> &'static str { REASON_NOT_ALLOWED_LOCAL => "Sandbox policy blocks local/private network addresses.", REASON_DENIED => "Domain denied by the sandbox policy.", REASON_METHOD_NOT_ALLOWED => "Method not allowed in limited mode.", + REASON_MITM_HOOK_DENIED => "HTTPS request denied by MITM hook policy.", REASON_MITM_REQUIRED => "MITM required for limited HTTPS.", REASON_PROXY_DISABLED => "network proxy is disabled", _ => "Request blocked by network policy.", diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index 11e3804baf..60894d5d5b 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -3,6 +3,9 @@ use crate::config::NetworkMode; use crate::config::NetworkProxyConfig; use crate::config::ValidatedUnixSocketPath; use crate::mitm::MitmState; +use crate::mitm_hook::HookEvaluation; +use crate::mitm_hook::MitmHooksByHost; +use crate::mitm_hook::evaluate_mitm_hooks; use crate::policy::Host; use crate::policy::is_loopback_host; use crate::policy::is_non_public_ip; @@ -159,6 +162,7 @@ pub struct ConfigState { pub allow_set: GlobSet, pub deny_set: GlobSet, pub mitm: Option>, + pub mitm_hooks: MitmHooksByHost, pub constraints: NetworkProxyConstraints, pub blocked: VecDeque, pub blocked_total: u64, @@ -585,6 +589,22 @@ impl NetworkProxyState { Ok(guard.mitm.clone()) } + pub(crate) async fn evaluate_mitm_hook_request( + &self, + host: &str, + req: &rama_http::Request, + ) -> Result { + self.reload_if_needed().await?; + let guard = self.state.read().await; + Ok(evaluate_mitm_hooks(&guard.mitm_hooks, host, req)) + } + + pub async fn host_has_mitm_hooks(&self, host: &str) -> Result { + self.reload_if_needed().await?; + let guard = self.state.read().await; + Ok(guard.mitm_hooks.contains_key(&normalize_host(host))) + } + pub async fn add_allowed_domain(&self, host: &str) -> Result<()> { self.update_domain_list(host, DomainListKind::Allow).await } @@ -846,9 +866,23 @@ pub(crate) fn network_proxy_state_for_policy( mut network: crate::config::NetworkProxySettings, ) -> NetworkProxyState { network.enabled = true; - network.mode = NetworkMode::Full; let config = NetworkProxyConfig { network }; - let state = build_config_state(config, NetworkProxyConstraints::default()).unwrap(); + let state = ConfigState { + allow_set: crate::policy::compile_allowlist_globset( + &config.network.allowed_domains().unwrap_or_default(), + ) + .unwrap(), + blocked: VecDeque::new(), + blocked_total: 0, + config: config.clone(), + constraints: NetworkProxyConstraints::default(), + deny_set: crate::policy::compile_denylist_globset( + &config.network.denied_domains().unwrap_or_default(), + ) + .unwrap(), + mitm: None, + mitm_hooks: crate::mitm_hook::compile_mitm_hooks(&config).unwrap(), + }; NetworkProxyState::with_reloader(state, Arc::new(NoopReloader)) } diff --git a/codex-rs/network-proxy/src/state.rs b/codex-rs/network-proxy/src/state.rs index 67a10d3bf5..32cdfab149 100644 --- a/codex-rs/network-proxy/src/state.rs +++ b/codex-rs/network-proxy/src/state.rs @@ -5,6 +5,7 @@ use crate::config::NetworkUnixSocketPermissions; use crate::mitm::MitmState; use crate::mitm::MitmUpstreamConfig; use crate::mitm_hook::MitmHookConfig; +use crate::mitm_hook::compile_mitm_hooks; use crate::mitm_hook::validate_mitm_hook_config; use crate::policy::DomainPattern; use crate::policy::compile_allowlist_globset; @@ -71,6 +72,7 @@ pub fn build_config_state( .map_err(NetworkProxyConstraintError::into_anyhow)?; let deny_set = compile_denylist_globset(&denied_domains)?; let allow_set = compile_allowlist_globset(&allowed_domains)?; + let mitm_hooks = compile_mitm_hooks(&config)?; let mitm = if config.network.mitm { Some(Arc::new(MitmState::new(MitmUpstreamConfig { allow_upstream_proxy: config.network.allow_upstream_proxy, @@ -84,6 +86,7 @@ pub fn build_config_state( allow_set, deny_set, mitm, + mitm_hooks, constraints, blocked: std::collections::VecDeque::new(), blocked_total: 0,