diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 37ff0ca4f2..41946890f6 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1693,6 +1693,7 @@ impl CodexMessageProcessor { network: started_network_proxy .as_ref() .map(codex_core::config::StartedNetworkProxy::proxy), + network_owner_id: None, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level, windows_sandbox_private_desktop: self diff --git a/codex-rs/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index e1a9cb3def..1e2bcc7ef4 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -732,6 +732,7 @@ mod tests { cwd: PathBuf::from("."), env: HashMap::new(), network: None, + network_owner_id: None, expiration: ExecExpiration::DefaultTimeout, capture_policy: codex_core::exec::ExecCapturePolicy::ShellTool, sandbox: SandboxType::WindowsRestrictedToken, @@ -845,6 +846,7 @@ mod tests { cwd: PathBuf::from("."), env: HashMap::new(), network: None, + network_owner_id: None, expiration: ExecExpiration::Cancellation(CancellationToken::new()), capture_policy: codex_core::exec::ExecCapturePolicy::ShellTool, sandbox: SandboxType::None, diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 5d6769e0ea..21c6aec33e 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -5033,6 +5033,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { capture_policy: ExecCapturePolicy::ShellTool, env: HashMap::new(), network: None, + network_owner_id: None, sandbox_permissions, windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context @@ -5051,6 +5052,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { capture_policy: ExecCapturePolicy::ShellTool, env: HashMap::new(), network: None, + network_owner_id: None, windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context .config diff --git a/codex-rs/core/src/codex_tests_guardian.rs b/codex-rs/core/src/codex_tests_guardian.rs index af0fccc9ac..8eea077dcd 100644 --- a/codex-rs/core/src/codex_tests_guardian.rs +++ b/codex-rs/core/src/codex_tests_guardian.rs @@ -128,6 +128,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid capture_policy: ExecCapturePolicy::ShellTool, env: HashMap::new(), network: None, + network_owner_id: None, sandbox_permissions: SandboxPermissions::WithAdditionalPermissions, windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 9be0518fa0..ce53baf98f 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -81,6 +81,7 @@ pub struct ExecParams { pub capture_policy: ExecCapturePolicy, pub env: HashMap, pub network: Option, + pub network_owner_id: Option, pub sandbox_permissions: SandboxPermissions, pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, @@ -263,6 +264,7 @@ pub fn build_exec_request( expiration, capture_policy, network, + network_owner_id, sandbox_permissions, windows_sandbox_level, windows_sandbox_private_desktop, @@ -270,7 +272,7 @@ pub fn build_exec_request( arg0: _, } = params; if let Some(network) = network.as_ref() { - network.apply_to_env(&mut env); + network.apply_to_env_for_owner(&mut env, network_owner_id.as_deref()); } let (program, args) = command.split_first().ok_or_else(|| { CodexErr::Io(io::Error::new( @@ -301,6 +303,7 @@ pub fn build_exec_request( sandbox: sandbox_type, enforce_managed_network, network: network.as_ref(), + network_owner_id: network_owner_id.as_deref(), sandbox_policy_cwd: sandbox_cwd, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, @@ -324,6 +327,7 @@ pub(crate) async fn execute_exec_request( cwd, env, network, + network_owner_id, expiration, capture_policy, sandbox, @@ -345,6 +349,7 @@ pub(crate) async fn execute_exec_request( capture_policy, env, network: network.clone(), + network_owner_id, sandbox_permissions, windows_sandbox_level, windows_sandbox_private_desktop, @@ -448,6 +453,7 @@ async fn exec_windows_sandbox( cwd, mut env, network, + network_owner_id, expiration, capture_policy, windows_sandbox_level, @@ -455,7 +461,7 @@ async fn exec_windows_sandbox( .. } = params; if let Some(network) = network.as_ref() { - network.apply_to_env(&mut env); + network.apply_to_env_for_owner(&mut env, network_owner_id.as_deref()); } // TODO(iceweasel-oai): run_windows_sandbox_capture should support all @@ -838,6 +844,7 @@ async fn exec( cwd, mut env, network, + network_owner_id, arg0, expiration, capture_policy, @@ -845,7 +852,7 @@ async fn exec( .. } = params; if let Some(network) = network.as_ref() { - network.apply_to_env(&mut env); + network.apply_to_env_for_owner(&mut env, network_owner_id.as_deref()); } let (program, args) = command.split_first().ok_or_else(|| { diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index fc312ec88e..ca0d0c5ef9 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -259,6 +259,7 @@ async fn exec_full_buffer_capture_ignores_expiration() -> Result<()> { capture_policy: ExecCapturePolicy::FullBuffer, env, network: None, + network_owner_id: None, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -298,6 +299,7 @@ async fn exec_full_buffer_capture_keeps_io_drain_timeout_when_descendant_holds_p capture_policy: ExecCapturePolicy::FullBuffer, env: std::env::vars().collect(), network: None, + network_owner_id: None, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -348,6 +350,7 @@ async fn process_exec_tool_call_preserves_full_buffer_capture_policy() -> Result capture_policy: ExecCapturePolicy::FullBuffer, env: std::env::vars().collect(), network: None, + network_owner_id: None, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -588,6 +591,7 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()> capture_policy: ExecCapturePolicy::ShellTool, env, network: None, + network_owner_id: None, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -646,6 +650,7 @@ async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> { capture_policy: ExecCapturePolicy::ShellTool, env, network: None, + network_owner_id: None, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, diff --git a/codex-rs/core/src/network_policy_decision_tests.rs b/codex-rs/core/src/network_policy_decision_tests.rs index ebb17f724f..404dca6cbd 100644 --- a/codex-rs/core/src/network_policy_decision_tests.rs +++ b/codex-rs/core/src/network_policy_decision_tests.rs @@ -156,6 +156,7 @@ fn denied_network_policy_message_requires_deny_decision() { let blocked = BlockedRequest { host: "example.com".to_string(), reason: "not_allowed".to_string(), + network_owner_id: None, client: None, method: Some("GET".to_string()), mode: None, @@ -173,6 +174,7 @@ fn denied_network_policy_message_for_denylist_block_is_explicit() { let blocked = BlockedRequest { host: "example.com".to_string(), reason: "denied".to_string(), + network_owner_id: None, client: None, method: Some("GET".to_string()), mode: None, diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 277ff2b241..81246c0bc6 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -68,6 +68,7 @@ pub struct ExecRequest { pub cwd: PathBuf, pub env: HashMap, pub network: Option, + pub network_owner_id: Option, pub expiration: ExecExpiration, pub capture_policy: ExecCapturePolicy, pub sandbox: SandboxType, @@ -94,6 +95,7 @@ pub(crate) struct SandboxTransformRequest<'a> { // TODO(viyatb): Evaluate switching this to Option> // to make shared ownership explicit across runtime/sandbox plumbing. pub network: Option<&'a NetworkProxy>, + pub network_owner_id: Option<&'a str>, pub sandbox_policy_cwd: &'a Path, #[cfg(target_os = "macos")] pub macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>, @@ -592,6 +594,7 @@ impl SandboxManager { sandbox, enforce_managed_network, network, + network_owner_id, sandbox_policy_cwd, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions, @@ -709,6 +712,7 @@ impl SandboxManager { cwd: spec.cwd, env, network: network.cloned(), + network_owner_id: network_owner_id.map(ToString::to_string), expiration: spec.expiration, capture_policy: spec.capture_policy, sandbox, diff --git a/codex-rs/core/src/sandboxing/mod_tests.rs b/codex-rs/core/src/sandboxing/mod_tests.rs index 9a7a34e49d..f93108abc9 100644 --- a/codex-rs/core/src/sandboxing/mod_tests.rs +++ b/codex-rs/core/src/sandboxing/mod_tests.rs @@ -171,6 +171,7 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() sandbox: SandboxType::None, enforce_managed_network: false, network: None, + network_owner_id: None, sandbox_policy_cwd: cwd.as_path(), #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, @@ -541,6 +542,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { sandbox: SandboxType::None, enforce_managed_network: false, network: None, + network_owner_id: None, sandbox_policy_cwd: cwd.as_path(), #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, @@ -615,6 +617,7 @@ fn transform_additional_permissions_preserves_denied_entries() { sandbox: SandboxType::None, enforce_managed_network: false, network: None, + network_owner_id: None, sandbox_policy_cwd: cwd.as_path(), #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 6b42be3cef..f049f062ed 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -163,6 +163,7 @@ pub(crate) async fn execute_user_shell_command( Some(session.conversation_id), ), network: turn_context.network.clone(), + network_owner_id: None, // TODO(zhao-oai): Now that we have ExecExpiration::Cancellation, we // should use that instead of an "arbitrarily large" timeout here. expiration: USER_SHELL_TIMEOUT_MS.into(), diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index 1d799da7e7..35ebcea37b 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -215,6 +215,7 @@ impl ToolHandler for ApplyPatchHandler { session: session.clone(), turn: turn.clone(), call_id: call_id.clone(), + network_approval_owner_id: None, tool_name: tool_name.to_string(), }; let out = orchestrator @@ -319,6 +320,7 @@ pub(crate) async fn intercept_apply_patch( session: session.clone(), turn: turn.clone(), call_id: call_id.to_string(), + network_approval_owner_id: None, tool_name: tool_name.to_string(), }; let out = orchestrator diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index 446ca100b7..14bf8717a6 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -74,6 +74,7 @@ impl ShellHandler { capture_policy: ExecCapturePolicy::ShellTool, env: create_env(&turn_context.shell_environment_policy, Some(thread_id)), network: turn_context.network.clone(), + network_owner_id: None, sandbox_permissions: params.sandbox_permissions.unwrap_or_default(), windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context @@ -129,6 +130,7 @@ impl ShellCommandHandler { capture_policy: ExecCapturePolicy::ShellTool, env: create_env(&turn_context.shell_environment_policy, Some(thread_id)), network: turn_context.network.clone(), + network_owner_id: None, sandbox_permissions: params.sandbox_permissions.unwrap_or_default(), windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context @@ -474,6 +476,7 @@ impl ShellHandler { session: session.clone(), turn: turn.clone(), call_id: call_id.clone(), + network_approval_owner_id: None, tool_name, }; let out = orchestrator diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index 4f7c3d7436..7c15a87f18 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -1067,6 +1067,7 @@ impl JsReplManager { sandbox: sandbox_type, enforce_managed_network: has_managed_network_requirements, network: None, + network_owner_id: None, sandbox_policy_cwd: &turn.cwd, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index 9e92a6b00f..a42652a560 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -64,6 +64,10 @@ impl ActiveNetworkApproval { self.mode } + pub(crate) fn owner_id(&self) -> Option<&str> { + self.registration_id.as_deref() + } + pub(crate) fn into_deferred(self) -> Option { match (self.mode, self.registration_id) { (NetworkApprovalMode::Deferred, Some(registration_id)) => { @@ -213,6 +217,18 @@ impl NetworkApprovalService { call_outcomes.remove(registration_id); } + async fn resolve_owner_call( + &self, + network_owner_id: Option<&str>, + ) -> Option> { + if let Some(network_owner_id) = network_owner_id { + let active_calls = self.active_calls.lock().await; + return active_calls.get(network_owner_id).cloned(); + } + + self.resolve_single_active_call().await + } + async fn resolve_single_active_call(&self) -> Option> { let active_calls = self.active_calls.lock().await; if active_calls.len() == 1 { @@ -236,8 +252,12 @@ impl NetworkApprovalService { (created, true) } - async fn record_outcome_for_single_active_call(&self, outcome: NetworkApprovalOutcome) { - let Some(owner_call) = self.resolve_single_active_call().await else { + async fn record_outcome_for_owner_call( + &self, + network_owner_id: Option<&str>, + outcome: NetworkApprovalOutcome, + ) { + let Some(owner_call) = self.resolve_owner_call(network_owner_id).await else { return; }; self.record_call_outcome(&owner_call.registration_id, outcome) @@ -265,8 +285,11 @@ impl NetworkApprovalService { return; }; - self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy(message)) - .await; + self.record_outcome_for_owner_call( + blocked.network_owner_id.as_deref(), + NetworkApprovalOutcome::DeniedByPolicy(message), + ) + .await; } async fn active_turn_context(session: &Session) -> Option> { @@ -328,9 +351,10 @@ impl NetworkApprovalService { pending.set_decision(PendingApprovalDecision::Deny).await; let mut pending_approvals = self.pending_host_approvals.lock().await; pending_approvals.remove(&key); - self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy( - policy_denial_message, - )) + self.record_outcome_for_owner_call( + request.network_owner_id.as_deref(), + NetworkApprovalOutcome::DeniedByPolicy(policy_denial_message), + ) .await; return NetworkDecision::deny(REASON_NOT_ALLOWED); }; @@ -338,9 +362,10 @@ impl NetworkApprovalService { pending.set_decision(PendingApprovalDecision::Deny).await; let mut pending_approvals = self.pending_host_approvals.lock().await; pending_approvals.remove(&key); - self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy( - policy_denial_message, - )) + self.record_outcome_for_owner_call( + request.network_owner_id.as_deref(), + NetworkApprovalOutcome::DeniedByPolicy(policy_denial_message), + ) .await; return NetworkDecision::deny(REASON_NOT_ALLOWED); } @@ -349,7 +374,9 @@ impl NetworkApprovalService { host: request.host.clone(), protocol, }; - let owner_call = self.resolve_single_active_call().await; + let owner_call = self + .resolve_owner_call(request.network_owner_id.as_deref()) + .await; let approval_decision = if routes_approval_to_guardian(&turn_context) { // TODO(ccunningham): Attach guardian network reviews to the reviewed tool item // lifecycle instead of this temporary standalone network approval id. diff --git a/codex-rs/core/src/tools/network_approval_tests.rs b/codex-rs/core/src/tools/network_approval_tests.rs index ad01a45bbd..6f9100f470 100644 --- a/codex-rs/core/src/tools/network_approval_tests.rs +++ b/codex-rs/core/src/tools/network_approval_tests.rs @@ -180,9 +180,14 @@ fn only_never_policy_disables_network_approval_flow() { } fn denied_blocked_request(host: &str) -> BlockedRequest { + denied_blocked_request_with_owner(host, None) +} + +fn denied_blocked_request_with_owner(host: &str, network_owner_id: Option<&str>) -> BlockedRequest { BlockedRequest::new(BlockedRequestArgs { host: host.to_string(), reason: "not_allowed".to_string(), + network_owner_id: network_owner_id.map(ToString::to_string), client: None, method: None, mode: None, @@ -249,3 +254,65 @@ async fn record_blocked_request_ignores_ambiguous_unattributed_blocked_requests( assert_eq!(service.take_call_outcome("registration-1").await, None); assert_eq!(service.take_call_outcome("registration-2").await, None); } + +#[tokio::test] +async fn resolve_owner_call_uses_network_owner_id_when_multiple_calls_are_active() { + let service = NetworkApprovalService::default(); + service + .register_call( + "registration-1".to_string(), + "turn-1".to_string(), + "command-1".to_string(), + ) + .await; + service + .register_call( + "registration-2".to_string(), + "turn-2".to_string(), + "command-2".to_string(), + ) + .await; + + let owner_call = service + .resolve_owner_call(Some("registration-2")) + .await + .expect("owner call should resolve"); + + assert_eq!(owner_call.registration_id, "registration-2"); + assert_eq!(owner_call.turn_id, "turn-2"); + assert_eq!(owner_call.parent_tool_item_id, "command-2"); +} + +#[tokio::test] +async fn record_blocked_request_uses_network_owner_id_when_multiple_calls_are_active() { + let service = NetworkApprovalService::default(); + service + .register_call( + "registration-1".to_string(), + "turn-1".to_string(), + "command-1".to_string(), + ) + .await; + service + .register_call( + "registration-2".to_string(), + "turn-2".to_string(), + "command-2".to_string(), + ) + .await; + + service + .record_blocked_request(denied_blocked_request_with_owner( + "example.com", + Some("registration-2"), + )) + .await; + + assert_eq!(service.take_call_outcome("registration-1").await, None); + assert_eq!( + service.take_call_outcome("registration-2").await, + Some(NetworkApprovalOutcome::DeniedByPolicy( + "Network access to \"example.com\" was blocked: domain is not on the allowlist for the current sandbox mode.".to_string() + )) + ); +} diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index 4b53ac156f..b4250afd87 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -13,6 +13,7 @@ use crate::guardian::GUARDIAN_REJECTION_MESSAGE; use crate::guardian::routes_approval_to_guardian; use crate::network_policy_decision::network_approval_context_from_payload; use crate::sandboxing::SandboxManager; +use crate::tools::network_approval::ActiveNetworkApproval; use crate::tools::network_approval::DeferredNetworkApproval; use crate::tools::network_approval::NetworkApprovalMode; use crate::tools::network_approval::begin_network_approval; @@ -64,11 +65,16 @@ impl ToolOrchestrator { tool.network_approval_spec(req, tool_ctx), ) .await; + let network_approval_owner_id = network_approval + .as_ref() + .and_then(ActiveNetworkApproval::owner_id) + .map(ToString::to_string); let attempt_tool_ctx = ToolCtx { session: tool_ctx.session.clone(), turn: tool_ctx.turn.clone(), call_id: tool_ctx.call_id.clone(), + network_approval_owner_id, tool_name: tool_ctx.tool_name.clone(), }; let run_result = tool.run(req, attempt, &attempt_tool_ctx).await; diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index f1e9912bc5..151b5d06a3 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -208,7 +208,7 @@ impl ToolRuntime for ApplyPatchRuntime { ) -> Result { let spec = Self::build_command_spec(req, &ctx.turn.config.codex_home)?; let env = attempt - .env_for(spec, /*network*/ None) + .env_for(spec, /*network*/ None, /*network_owner_id*/ None) .map_err(|err| ToolError::Codex(err.into()))?; let out = execute_env(env, Self::stdout_stream(ctx)) .await diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 6ff8349b53..01f6493a60 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -253,7 +253,11 @@ impl ToolRuntime for ShellRuntime { req.justification.clone(), )?; let env = attempt - .env_for(spec, req.network.as_ref()) + .env_for( + spec, + req.network.as_ref(), + ctx.network_approval_owner_id.as_deref(), + ) .map_err(|err| ToolError::Codex(err.into()))?; let out = execute_env(env, Self::stdout_stream(ctx)) .await diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 948018dae6..4e8da53a0c 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -117,13 +117,18 @@ pub(super) async fn try_run_zsh_fork( req.justification.clone(), )?; let sandbox_exec_request = attempt - .env_for(spec, req.network.as_ref()) + .env_for( + spec, + req.network.as_ref(), + ctx.network_approval_owner_id.as_deref(), + ) .map_err(|err| ToolError::Codex(err.into()))?; let crate::sandboxing::ExecRequest { command, cwd: sandbox_cwd, env: sandbox_env, network: sandbox_network, + network_owner_id, expiration: _sandbox_expiration, capture_policy: _capture_policy, sandbox, @@ -153,6 +158,7 @@ pub(super) async fn try_run_zsh_fork( sandbox, env: sandbox_env, network: sandbox_network, + network_owner_id, windows_sandbox_level, sandbox_permissions, justification, @@ -259,6 +265,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( sandbox: exec_request.sandbox, env: exec_request.env.clone(), network: exec_request.network.clone(), + network_owner_id: exec_request.network_owner_id.clone(), windows_sandbox_level: exec_request.windows_sandbox_level, sandbox_permissions: exec_request.sandbox_permissions, justification: exec_request.justification.clone(), @@ -856,6 +863,7 @@ struct CoreShellCommandExecutor { sandbox: SandboxType, env: HashMap, network: Option, + network_owner_id: Option, windows_sandbox_level: WindowsSandboxLevel, sandbox_permissions: SandboxPermissions, justification: Option, @@ -904,6 +912,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { cwd: self.cwd.clone(), env: exec_env, network: self.network.clone(), + network_owner_id: self.network_owner_id.clone(), expiration: ExecExpiration::Cancellation(cancel_rx), capture_policy: ExecCapturePolicy::ShellTool, sandbox: self.sandbox, @@ -1060,6 +1069,7 @@ impl CoreShellCommandExecutor { sandbox, enforce_managed_network: self.network.is_some(), network: self.network.as_ref(), + network_owner_id: self.network_owner_id.as_deref(), sandbox_policy_cwd: &self.sandbox_policy_cwd, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions, @@ -1069,7 +1079,10 @@ impl CoreShellCommandExecutor { windows_sandbox_private_desktop: false, })?; if let Some(network) = exec_request.network.as_ref() { - network.apply_to_env(&mut exec_request.env); + network.apply_to_env_for_owner( + &mut exec_request.env, + exec_request.network_owner_id.as_deref(), + ); } Ok(PreparedExec { diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index 37c1b53a13..ec26724af4 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -655,6 +655,7 @@ async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions cwd: cwd.to_path_buf(), env: HashMap::new(), network: None, + network_owner_id: None, sandbox: SandboxType::None, sandbox_policy: SandboxPolicy::new_read_only_policy(), file_system_sandbox_policy: read_only_file_system_sandbox_policy(), @@ -707,6 +708,7 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions() cwd: cwd.to_path_buf(), env: HashMap::new(), network: None, + network_owner_id: None, sandbox: SandboxType::None, sandbox_policy: SandboxPolicy::DangerFullAccess, file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(), @@ -782,6 +784,7 @@ async fn prepare_escalated_exec_permission_profile_unions_turn_and_requested_mac cwd: cwd.to_path_buf(), env: HashMap::new(), network: None, + network_owner_id: None, sandbox: SandboxType::None, sandbox_policy: sandbox_policy.clone(), file_system_sandbox_policy: read_only_file_system_sandbox_policy(), diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 49ef0e6385..b17f9c7dde 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -207,7 +207,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt let mut env = req.env.clone(); if let Some(network) = req.network.as_ref() { - network.apply_to_env(&mut env); + network.apply_to_env_for_owner(&mut env, ctx.network_approval_owner_id.as_deref()); } if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode { let spec = build_command_spec( @@ -221,7 +221,11 @@ impl<'a> ToolRuntime for UnifiedExecRunt ) .map_err(|_| ToolError::Rejected("missing command line for PTY".to_string()))?; let exec_env = attempt - .env_for(spec, req.network.as_ref()) + .env_for( + spec, + req.network.as_ref(), + ctx.network_approval_owner_id.as_deref(), + ) .map_err(|err| ToolError::Codex(err.into()))?; match zsh_fork_backend::maybe_prepare_unified_exec( req, @@ -269,7 +273,11 @@ impl<'a> ToolRuntime for UnifiedExecRunt ) .map_err(|_| ToolError::Rejected("missing command line for PTY".to_string()))?; let exec_env = attempt - .env_for(spec, req.network.as_ref()) + .env_for( + spec, + req.network.as_ref(), + ctx.network_approval_owner_id.as_deref(), + ) .map_err(|err| ToolError::Codex(err.into()))?; self.manager .open_session_with_exec_env(&exec_env, req.tty, Box::new(NoopSpawnLifecycle)) diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index bf386a12d0..3251f83302 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -300,6 +300,7 @@ pub(crate) struct ToolCtx { pub session: Arc, pub turn: Arc, pub call_id: String, + pub network_approval_owner_id: Option, pub tool_name: String, } @@ -341,6 +342,7 @@ impl<'a> SandboxAttempt<'a> { &self, spec: CommandSpec, network: Option<&NetworkProxy>, + network_owner_id: Option<&str>, ) -> Result { self.manager .transform(crate::sandboxing::SandboxTransformRequest { @@ -351,6 +353,7 @@ impl<'a> SandboxAttempt<'a> { sandbox: self.sandbox, enforce_managed_network: self.enforce_managed_network, network, + network_owner_id, sandbox_policy_cwd: self.sandbox_cwd, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 52d668c000..0ea2e653e2 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -627,6 +627,7 @@ impl UnifiedExecProcessManager { session: context.session.clone(), turn: context.turn.clone(), call_id: context.call_id.clone(), + network_approval_owner_id: None, tool_name: "exec_command".to_string(), }; orchestrator diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index 069e824ee5..22b19c71b7 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -41,6 +41,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result(req: &Request) -> Option { + req.headers() + .typed_get::>() + .map(|header| header.0.username().to_string()) +} + async fn http_connect_accept( policy_decider: Option>, mut req: Request, @@ -173,6 +181,7 @@ async fn http_connect_accept( } let client = client_addr(&req); + let network_owner_id = proxy_authorization_network_owner_id(&req); let enabled = app_state .enabled() .await @@ -185,6 +194,7 @@ async fn http_connect_accept( host, authority.port, client_addr(&req), + network_owner_id, Some("CONNECT".to_string()), NetworkProtocol::HttpsConnect, /*audit_endpoint_override*/ None, @@ -196,6 +206,7 @@ async fn http_connect_accept( protocol: NetworkProtocol::HttpsConnect, host: host.clone(), port: authority.port, + network_owner_id: network_owner_id.clone(), client_addr: client.clone(), method: Some("CONNECT".to_string()), command: None, @@ -220,6 +231,7 @@ async fn http_connect_accept( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: reason.clone(), + network_owner_id, client: client.clone(), method: Some("CONNECT".to_string()), mode: None, @@ -283,6 +295,7 @@ async fn http_connect_accept( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: REASON_MITM_REQUIRED.to_string(), + network_owner_id: None, client: client.clone(), method: Some("CONNECT".to_string()), mode: Some(NetworkMode::Limited), @@ -432,6 +445,7 @@ async fn http_plain_proxy( } }; let client = client_addr(&req); + let network_owner_id = proxy_authorization_network_owner_id(&req); let method_allowed = match app_state .method_allowed(req.method().as_str()) .await @@ -471,6 +485,7 @@ async fn http_plain_proxy( socket_path, /*port*/ 0, client_addr(&req), + network_owner_id.clone(), Some(req.method().as_str().to_string()), NetworkProtocol::Http, Some(("unix-socket", 0)), @@ -616,6 +631,7 @@ async fn http_plain_proxy( host, port, client_addr(&req), + network_owner_id.clone(), Some(req.method().as_str().to_string()), NetworkProtocol::Http, /*audit_endpoint_override*/ None, @@ -627,6 +643,7 @@ async fn http_plain_proxy( protocol: NetworkProtocol::Http, host: host.clone(), port, + network_owner_id: network_owner_id.clone(), client_addr: client.clone(), method: Some(req.method().as_str().to_string()), command: None, @@ -651,6 +668,7 @@ async fn http_plain_proxy( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: reason.clone(), + network_owner_id: network_owner_id.clone(), client: client.clone(), method: Some(req.method().as_str().to_string()), mode: None, @@ -696,6 +714,7 @@ async fn http_plain_proxy( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: REASON_METHOD_NOT_ALLOWED.to_string(), + network_owner_id, client: client.clone(), method: Some(req.method().as_str().to_string()), mode: Some(NetworkMode::Limited), @@ -889,6 +908,7 @@ async fn proxy_disabled_response( host: String, port: u16, client: Option, + network_owner_id: Option, method: Option, protocol: NetworkProtocol, audit_endpoint_override: Option<(&str, u16)>, @@ -913,6 +933,7 @@ async fn proxy_disabled_response( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: blocked_host, reason: REASON_PROXY_DISABLED.to_string(), + network_owner_id, client, method, mode: None, diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index 1093a14aa4..f896bf72e3 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -5,6 +5,7 @@ mod config; mod http_proxy; mod mitm; mod network_policy; +mod owner_identity; mod policy; mod proxy; mod reasons; diff --git a/codex-rs/network-proxy/src/mitm.rs b/codex-rs/network-proxy/src/mitm.rs index 8f20afaa3d..5b37e1207c 100644 --- a/codex-rs/network-proxy/src/mitm.rs +++ b/codex-rs/network-proxy/src/mitm.rs @@ -289,6 +289,7 @@ async fn mitm_blocking_response( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: policy.target_host.clone(), reason: reason.to_string(), + network_owner_id: None, client: client.clone(), method: Some(method.clone()), mode: Some(policy.mode), @@ -311,6 +312,7 @@ async fn mitm_blocking_response( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: policy.target_host.clone(), reason: REASON_METHOD_NOT_ALLOWED.to_string(), + network_owner_id: None, client: client.clone(), method: Some(method.clone()), mode: Some(policy.mode), diff --git a/codex-rs/network-proxy/src/network_policy.rs b/codex-rs/network-proxy/src/network_policy.rs index e759e75c0d..0aadc10bfd 100644 --- a/codex-rs/network-proxy/src/network_policy.rs +++ b/codex-rs/network-proxy/src/network_policy.rs @@ -79,6 +79,7 @@ pub struct NetworkPolicyRequest { pub protocol: NetworkProtocol, pub host: String, pub port: u16, + pub network_owner_id: Option, pub client_addr: Option, pub method: Option, pub command: Option, @@ -89,6 +90,7 @@ pub struct NetworkPolicyRequestArgs { pub protocol: NetworkProtocol, pub host: String, pub port: u16, + pub network_owner_id: Option, pub client_addr: Option, pub method: Option, pub command: Option, @@ -101,6 +103,7 @@ impl NetworkPolicyRequest { protocol, host, port, + network_owner_id, client_addr, method, command, @@ -110,6 +113,7 @@ impl NetworkPolicyRequest { protocol, host, port, + network_owner_id, client_addr, method, command, @@ -625,6 +629,7 @@ mod tests { protocol: NetworkProtocol::Http, host: "example.com".to_string(), port: 80, + network_owner_id: None, client_addr: None, method: None, command: None, @@ -685,6 +690,7 @@ mod tests { protocol: NetworkProtocol::Http, host: "blocked.com".to_string(), port: 80, + network_owner_id: None, client_addr: Some("127.0.0.1:1234".to_string()), method: Some("GET".to_string()), command: None, @@ -726,6 +732,7 @@ mod tests { protocol: NetworkProtocol::Http, host: "example.com".to_string(), port: 80, + network_owner_id: None, client_addr: None, method: Some("GET".to_string()), command: None, @@ -776,6 +783,7 @@ mod tests { protocol: NetworkProtocol::Http, host: "example.com".to_string(), port: 80, + network_owner_id: None, client_addr: None, method: Some("GET".to_string()), command: None, @@ -859,6 +867,7 @@ mod tests { protocol: NetworkProtocol::Http, host: "127.0.0.1".to_string(), port: 80, + network_owner_id: None, client_addr: None, method: Some("GET".to_string()), command: None, diff --git a/codex-rs/network-proxy/src/owner_identity.rs b/codex-rs/network-proxy/src/owner_identity.rs new file mode 100644 index 0000000000..3c898ec1d2 --- /dev/null +++ b/codex-rs/network-proxy/src/owner_identity.rs @@ -0,0 +1,38 @@ +use rama_core::extensions::Extensions; +use rama_core::extensions::ExtensionsRef; +use rama_http::headers::authorization::AuthoritySync; +use rama_net::user::Basic; +use rama_net::user::UserId; +use rama_net::user::authority::AuthorizeResult; +use rama_net::user::authority::Authorizer; + +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct ProxyOwnerAuthorizer; + +impl AuthoritySync for ProxyOwnerAuthorizer { + fn authorized(&self, ext: &mut Extensions, credentials: &Basic) -> bool { + ext.insert(UserId::Username(credentials.username().to_string())); + true + } +} + +impl Authorizer for ProxyOwnerAuthorizer { + type Error = std::convert::Infallible; + + async fn authorize(&self, credentials: Basic) -> AuthorizeResult { + let mut extensions = Extensions::new(); + extensions.insert(UserId::Username(credentials.username().to_string())); + AuthorizeResult { + credentials, + result: Ok(Some(extensions)), + } + } +} + +pub(crate) fn network_owner_id(input: &T) -> Option { + match input.extensions().get::() { + Some(UserId::Username(username)) => Some(username.clone()), + Some(UserId::Token(token)) => String::from_utf8(token.clone()).ok(), + Some(UserId::Anonymous) | None => None, + } +} diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 8f596f6842..8cf281d27f 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -305,15 +305,23 @@ fn set_env_keys(env: &mut HashMap, keys: &[&str], value: &str) { } } +fn proxy_url(scheme: &str, addr: SocketAddr, network_owner_id: Option<&str>) -> String { + match network_owner_id { + Some(network_owner_id) => format!("{scheme}://{network_owner_id}@{addr}"), + None => format!("{scheme}://{addr}"), + } +} + fn apply_proxy_env_overrides( env: &mut HashMap, http_addr: SocketAddr, socks_addr: SocketAddr, socks_enabled: bool, allow_local_binding: bool, + network_owner_id: Option<&str>, ) { - let http_proxy_url = format!("http://{http_addr}"); - let socks_proxy_url = format!("socks5h://{socks_addr}"); + let http_proxy_url = proxy_url("http", http_addr, network_owner_id); + let socks_proxy_url = proxy_url("socks5h", socks_addr, network_owner_id); env.insert( ALLOW_LOCAL_BINDING_ENV_KEY.to_string(), if allow_local_binding { @@ -414,6 +422,14 @@ impl NetworkProxy { } pub fn apply_to_env(&self, env: &mut HashMap) { + self.apply_to_env_for_owner(env, /*network_owner_id*/ None); + } + + pub fn apply_to_env_for_owner( + &self, + env: &mut HashMap, + network_owner_id: Option<&str>, + ) { // Enforce proxying for child processes. We intentionally override existing values so // command-level environment cannot bypass the managed proxy endpoint. apply_proxy_env_overrides( @@ -422,6 +438,7 @@ impl NetworkProxy { self.socks_addr, self.socks_enabled, self.allow_local_binding, + network_owner_id, ); } @@ -696,6 +713,7 @@ mod tests { SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), true, false, + /*network_owner_id*/ None, ); assert_eq!( @@ -746,6 +764,7 @@ mod tests { SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), false, true, + /*network_owner_id*/ None, ); assert_eq!( @@ -764,6 +783,7 @@ mod tests { SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), true, false, + /*network_owner_id*/ None, ); assert_eq!( @@ -809,6 +829,7 @@ mod tests { SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), true, false, + /*network_owner_id*/ None, ); assert_eq!( @@ -816,4 +837,26 @@ mod tests { Some(&"ssh -o ProxyCommand='tsh proxy ssh --cluster=dev %r@%h:%p'".to_string()) ); } + + #[test] + fn apply_proxy_env_overrides_includes_owner_credentials() { + 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("owner-1"), + ); + + assert_eq!( + env.get("HTTP_PROXY"), + Some(&"http://owner-1@127.0.0.1:3128".to_string()) + ); + assert_eq!( + env.get("ALL_PROXY"), + Some(&"socks5h://owner-1@127.0.0.1:8081".to_string()) + ); + } } diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index b634a8630e..d51729aed8 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -84,6 +84,8 @@ pub enum HostBlockDecision { pub struct BlockedRequest { pub host: String, pub reason: String, + #[serde(skip_serializing)] + pub network_owner_id: Option, pub client: Option, pub method: Option, pub mode: Option, @@ -100,6 +102,7 @@ pub struct BlockedRequest { pub struct BlockedRequestArgs { pub host: String, pub reason: String, + pub network_owner_id: Option, pub client: Option, pub method: Option, pub mode: Option, @@ -114,6 +117,7 @@ impl BlockedRequest { let BlockedRequestArgs { host, reason, + network_owner_id, client, method, mode, @@ -125,6 +129,7 @@ impl BlockedRequest { Self { host, reason, + network_owner_id, client, method, mode, @@ -994,6 +999,7 @@ mod tests { .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: "google.com".to_string(), reason: "not_allowed".to_string(), + network_owner_id: None, client: None, method: Some("GET".to_string()), mode: None, @@ -1034,6 +1040,7 @@ mod tests { .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: format!("example{idx}.com"), reason: "not_allowed".to_string(), + network_owner_id: None, client: None, method: Some("GET".to_string()), mode: None, @@ -1056,6 +1063,7 @@ mod tests { let entry = BlockedRequest { host: "google.com".to_string(), reason: "not_allowed".to_string(), + network_owner_id: None, client: Some("127.0.0.1".to_string()), method: Some("GET".to_string()), mode: Some(NetworkMode::Full), diff --git a/codex-rs/network-proxy/src/socks5.rs b/codex-rs/network-proxy/src/socks5.rs index 30ec4d2903..16b4f653ae 100644 --- a/codex-rs/network-proxy/src/socks5.rs +++ b/codex-rs/network-proxy/src/socks5.rs @@ -9,6 +9,8 @@ use crate::network_policy::NetworkPolicyRequestArgs; use crate::network_policy::NetworkProtocol; use crate::network_policy::emit_block_decision_audit_event; use crate::network_policy::evaluate_host_policy; +use crate::owner_identity::ProxyOwnerAuthorizer; +use crate::owner_identity::network_owner_id as extract_network_owner_id; use crate::policy::normalize_host; use crate::reasons::REASON_METHOD_NOT_ALLOWED; use crate::reasons::REASON_PROXY_DISABLED; @@ -105,7 +107,11 @@ async fn run_socks5_with_listener( }); let socks_connector = DefaultConnector::default().with_connector(policy_tcp_connector); - let base = Socks5Acceptor::new().with_connector(socks_connector); + let mut base = Socks5Acceptor::new(); + base.set_auth_optional(true); + let base = base + .with_connector(socks_connector) + .with_authorizer(ProxyOwnerAuthorizer); if enable_socks5_udp { let udp_state = state.clone(); @@ -150,6 +156,7 @@ async fn handle_socks5_tcp( .extensions() .get::() .map(|info| info.peer_addr().to_string()); + let network_owner_id = extract_network_owner_id(&req); match app_state.enabled().await { Ok(true) => {} @@ -175,6 +182,7 @@ async fn handle_socks5_tcp( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: REASON_PROXY_DISABLED.to_string(), + network_owner_id: network_owner_id.clone(), client: client.clone(), method: None, mode: None, @@ -217,6 +225,7 @@ async fn handle_socks5_tcp( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: REASON_METHOD_NOT_ALLOWED.to_string(), + network_owner_id: network_owner_id.clone(), client: client.clone(), method: None, mode: Some(NetworkMode::Limited), @@ -243,6 +252,7 @@ async fn handle_socks5_tcp( protocol: NetworkProtocol::Socks5Tcp, host: host.clone(), port, + network_owner_id: network_owner_id.clone(), client_addr: client.clone(), method: None, command: None, @@ -267,6 +277,7 @@ async fn handle_socks5_tcp( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: reason.clone(), + network_owner_id, client: client.clone(), method: None, mode: None, @@ -314,6 +325,7 @@ async fn inspect_socks5_udp( let client = extensions .get::() .map(|info| info.peer_addr().to_string()); + let network_owner_id = extract_network_owner_id(&extensions); match state.enabled().await { Ok(true) => {} @@ -339,6 +351,7 @@ async fn inspect_socks5_udp( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: REASON_PROXY_DISABLED.to_string(), + network_owner_id: network_owner_id.clone(), client: client.clone(), method: None, mode: None, @@ -381,6 +394,7 @@ async fn inspect_socks5_udp( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: REASON_METHOD_NOT_ALLOWED.to_string(), + network_owner_id: network_owner_id.clone(), client: client.clone(), method: None, mode: Some(NetworkMode::Limited), @@ -403,6 +417,7 @@ async fn inspect_socks5_udp( protocol: NetworkProtocol::Socks5Udp, host: host.clone(), port, + network_owner_id: network_owner_id.clone(), client_addr: client.clone(), method: None, command: None, @@ -427,6 +442,7 @@ async fn inspect_socks5_udp( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: reason.clone(), + network_owner_id, client: client.clone(), method: None, mode: None,