@@ -80,6 +80,9 @@ use tracing::error;
8080use tracing:: info;
8181use tracing:: warn;
8282
83+ #[ derive( Clone , Copy , Debug ) ]
84+ struct ConnectMitmEnabled ( bool ) ;
85+
8386pub async fn run_http_proxy (
8487 state : Arc < NetworkProxyState > ,
8588 addr : SocketAddr ,
@@ -256,10 +259,18 @@ async fn http_connect_accept(
256259 return Err ( text_response ( StatusCode :: INTERNAL_SERVER_ERROR , "error" ) ) ;
257260 }
258261 } ;
262+ let host_has_mitm_hooks = match app_state. host_has_mitm_hooks ( & host) . await {
263+ Ok ( has_hooks) => has_hooks,
264+ Err ( err) => {
265+ error ! ( "failed to inspect MITM hooks for {host}: {err}" ) ;
266+ return Err ( text_response ( StatusCode :: INTERNAL_SERVER_ERROR , "error" ) ) ;
267+ }
268+ } ;
269+ let connect_needs_mitm = mode == NetworkMode :: Limited || host_has_mitm_hooks;
259270
260- if mode == NetworkMode :: Limited && mitm_state. is_none ( ) {
261- // Limited mode is designed to be read-only. Without MITM, a CONNECT tunnel would hide the
262- // inner HTTP method/headers from the proxy, effectively bypassing method policy .
271+ if connect_needs_mitm && mitm_state. is_none ( ) {
272+ // CONNECT needs MITM whenever HTTPS policy depends on inner-request inspection, either for
273+ // limited-mode method enforcement or for host-specific MITM hooks .
263274 emit_http_block_decision_audit_event (
264275 & app_state,
265276 BlockDecisionAuditEventArgs {
@@ -286,7 +297,7 @@ async fn http_connect_accept(
286297 reason : REASON_MITM_REQUIRED . to_string ( ) ,
287298 client : client. clone ( ) ,
288299 method : Some ( "CONNECT" . to_string ( ) ) ,
289- mode : Some ( NetworkMode :: Limited ) ,
300+ mode : Some ( mode ) ,
290301 protocol : "http-connect" . to_string ( ) ,
291302 decision : Some ( details. decision . as_str ( ) . to_string ( ) ) ,
292303 source : Some ( details. source . as_str ( ) . to_string ( ) ) ,
@@ -295,14 +306,16 @@ async fn http_connect_accept(
295306 . await ;
296307 let client = client. as_deref ( ) . unwrap_or_default ( ) ;
297308 warn ! (
298- "CONNECT blocked; MITM required for read-only HTTPS in limited mode (client={client}, host={host}, mode=limited, allowed_methods=GET, HEAD, OPTIONS )"
309+ "CONNECT blocked; MITM required to enforce HTTPS policy (client={client}, host={host}, mode={mode:?}, hooked_host={host_has_mitm_hooks} )"
299310 ) ;
300311 return Err ( blocked_text_with_details ( REASON_MITM_REQUIRED , & details) ) ;
301312 }
302313
303314 req. extensions_mut ( ) . insert ( ProxyTarget ( authority) ) ;
315+ req. extensions_mut ( )
316+ . insert ( ConnectMitmEnabled ( connect_needs_mitm) ) ;
304317 req. extensions_mut ( ) . insert ( mode) ;
305- if let Some ( mitm_state) = mitm_state {
318+ if connect_needs_mitm && let Some ( mitm_state) = mitm_state {
306319 req. extensions_mut ( ) . insert ( mitm_state) ;
307320 }
308321
@@ -331,7 +344,10 @@ async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> {
331344 return Ok ( ( ) ) ;
332345 } ;
333346
334- if mode == NetworkMode :: Limited
347+ if upgraded
348+ . extensions ( )
349+ . get :: < ConnectMitmEnabled > ( )
350+ . is_some_and ( |enabled| enabled. 0 )
335351 && upgraded
336352 . extensions ( )
337353 . get :: < Arc < mitm:: MitmState > > ( )
@@ -1094,6 +1110,42 @@ mod tests {
10941110 assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
10951111 }
10961112
1113+ #[ tokio:: test]
1114+ async fn http_connect_accept_blocks_hooked_host_in_full_mode_without_mitm_state ( ) {
1115+ let mut policy = NetworkProxySettings {
1116+ mitm : true ,
1117+ mitm_hooks : vec ! [ crate :: mitm_hook:: MitmHookConfig {
1118+ host: "api.github.com" . to_string( ) ,
1119+ matcher: crate :: mitm_hook:: MitmHookMatchConfig {
1120+ methods: vec![ "POST" . to_string( ) ] ,
1121+ path_prefixes: vec![ "/repos/openai/" . to_string( ) ] ,
1122+ ..crate :: mitm_hook:: MitmHookMatchConfig :: default ( )
1123+ } ,
1124+ actions: crate :: mitm_hook:: MitmHookActionsConfig :: default ( ) ,
1125+ } ] ,
1126+ ..Default :: default ( )
1127+ } ;
1128+ policy. set_allowed_domains ( vec ! [ "api.github.com" . to_string( ) ] ) ;
1129+ let state = Arc :: new ( network_proxy_state_for_policy ( policy) ) ;
1130+
1131+ let mut req = Request :: builder ( )
1132+ . method ( Method :: CONNECT )
1133+ . uri ( "https://api.github.com:443" )
1134+ . header ( "host" , "api.github.com:443" )
1135+ . body ( Body :: empty ( ) )
1136+ . unwrap ( ) ;
1137+ req. extensions_mut ( ) . insert ( state) ;
1138+
1139+ let response = http_connect_accept ( /*policy_decider*/ None , req)
1140+ . await
1141+ . unwrap_err ( ) ;
1142+ assert_eq ! ( response. status( ) , StatusCode :: FORBIDDEN ) ;
1143+ assert_eq ! (
1144+ response. headers( ) . get( "x-proxy-error" ) . unwrap( ) ,
1145+ "blocked-by-mitm-required"
1146+ ) ;
1147+ }
1148+
10971149 #[ tokio:: test]
10981150 async fn http_proxy_listener_accepts_plain_http1_connect_requests ( ) {
10991151 let target_listener = TokioTcpListener :: bind ( ( Ipv4Addr :: LOCALHOST , 0 ) )
0 commit comments