diff --git a/crates/openshell-sandbox/src/identity.rs b/crates/openshell-sandbox/src/identity.rs index d27976ba..49809f95 100644 --- a/crates/openshell-sandbox/src/identity.rs +++ b/crates/openshell-sandbox/src/identity.rs @@ -16,6 +16,7 @@ use std::fs::Metadata; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::sync::Mutex; +use tracing::debug; #[derive(Clone)] struct FileFingerprint { @@ -100,6 +101,7 @@ impl BinaryIdentityCache { where F: FnMut(&Path) -> Result, { + let start = std::time::Instant::now(); let metadata = std::fs::metadata(path) .map_err(|error| miette::miette!("Failed to stat {}: {error}", path.display()))?; let fingerprint = FileFingerprint::from_metadata(&metadata); @@ -114,9 +116,20 @@ impl BinaryIdentityCache { if let Some(cached_binary) = &cached && cached_binary.fingerprint == fingerprint { + debug!( + " verify_or_cache: {}ms CACHE HIT path={}", + start.elapsed().as_millis(), + path.display() + ); return Ok(cached_binary.hash.clone()); } + debug!( + " verify_or_cache: CACHE MISS size={} path={}", + metadata.len(), + path.display() + ); + let current_hash = hash_file(path)?; let mut hashes = self @@ -143,6 +156,12 @@ impl BinaryIdentityCache { }, ); + debug!( + " verify_or_cache TOTAL (cold): {}ms path={}", + start.elapsed().as_millis(), + path.display() + ); + Ok(current_hash) } } diff --git a/crates/openshell-sandbox/src/procfs.rs b/crates/openshell-sandbox/src/procfs.rs index ece16c82..785a9489 100644 --- a/crates/openshell-sandbox/src/procfs.rs +++ b/crates/openshell-sandbox/src/procfs.rs @@ -6,10 +6,11 @@ //! Provides functions to resolve binary paths and compute file hashes //! for process-identity binding in the OPA proxy policy engine. -use miette::{IntoDiagnostic, Result}; +use miette::Result; use std::path::Path; #[cfg(target_os = "linux")] use std::path::PathBuf; +use tracing::debug; /// Read the binary path of a process via `/proc/{pid}/exe` symlink. /// @@ -229,8 +230,9 @@ fn parse_proc_net_tcp(pid: u32, peer_port: u16) -> Result { fn find_pid_by_socket_inode(inode: u64, entrypoint_pid: u32) -> Result { let target = format!("socket:[{inode}]"); - // First: scan descendants of the entrypoint process (targeted, most likely to succeed) + // First: scan descendants of the entrypoint process let descendants = collect_descendant_pids(entrypoint_pid); + for &pid in &descendants { if let Some(found) = check_pid_fds(pid, &target) { return Ok(found); @@ -238,7 +240,6 @@ fn find_pid_by_socket_inode(inode: u64, entrypoint_pid: u32) -> Result { } // Fallback: scan all of /proc in case the process isn't in the tree - // (e.g., if /proc//task//children wasn't available) if let Ok(proc_dir) = std::fs::read_dir("/proc") { for entry in proc_dir.flatten() { let name = entry.file_name(); @@ -318,9 +319,32 @@ fn collect_descendant_pids(root_pid: u32) -> Vec { /// same hash, or the request is denied. pub fn file_sha256(path: &Path) -> Result { use sha2::{Digest, Sha256}; + use std::io::Read; + + let start = std::time::Instant::now(); + let mut file = std::fs::File::open(path) + .map_err(|e| miette::miette!("Failed to open {}: {e}", path.display()))?; + let mut hasher = Sha256::new(); + let mut buf = [0u8; 65536]; + let mut total_read = 0u64; + loop { + let n = file + .read(&mut buf) + .map_err(|e| miette::miette!("Failed to read {}: {e}", path.display()))?; + if n == 0 { + break; + } + total_read += n as u64; + hasher.update(&buf[..n]); + } - let bytes = std::fs::read(path).into_diagnostic()?; - let hash = Sha256::digest(&bytes); + let hash = hasher.finalize(); + debug!( + " file_sha256: {}ms size={} path={}", + start.elapsed().as_millis(), + total_read, + path.display() + ); Ok(hex::encode(hash)) } diff --git a/crates/openshell-sandbox/src/proxy.rs b/crates/openshell-sandbox/src/proxy.rs index d662399b..639fd17e 100644 --- a/crates/openshell-sandbox/src/proxy.rs +++ b/crates/openshell-sandbox/src/proxy.rs @@ -336,15 +336,25 @@ async fn handle_tcp_connection( let peer_addr = client.peer_addr().into_diagnostic()?; let local_addr = client.local_addr().into_diagnostic()?; - // Evaluate OPA policy with process-identity binding - let decision = evaluate_opa_tcp( - peer_addr, - &opa_engine, - &identity_cache, - &entrypoint_pid, - &host_lc, - port, - ); + // Evaluate OPA policy with process-identity binding. + // Wrapped in spawn_blocking because identity resolution does heavy sync I/O: + // /proc scanning + SHA256 hashing of binaries (e.g. node at 124MB). + let opa_clone = opa_engine.clone(); + let cache_clone = identity_cache.clone(); + let pid_clone = entrypoint_pid.clone(); + let host_clone = host_lc.clone(); + let decision = tokio::task::spawn_blocking(move || { + evaluate_opa_tcp( + peer_addr, + &opa_clone, + &cache_clone, + &pid_clone, + &host_clone, + port, + ) + }) + .await + .map_err(|e| miette::miette!("identity resolution task panicked: {e}"))?; // Extract action string and matched policy for logging let (matched_policy, deny_reason) = match &decision.action { @@ -421,6 +431,7 @@ async fn handle_tcp_connection( let raw_allowed_ips = query_allowed_ips(&opa_engine, &decision, &host_lc, port); // Defense-in-depth: resolve DNS and reject connections to internal IPs. + let dns_connect_start = std::time::Instant::now(); let mut upstream = if !raw_allowed_ips.is_empty() { // allowed_ips mode: validate resolved IPs against CIDR allowlist. // Loopback and link-local are still always blocked. @@ -497,6 +508,11 @@ async fn handle_tcp_connection( } }; + debug!( + "handle_tcp_connection dns_resolve_and_tcp_connect: {}ms host={host_lc}", + dns_connect_start.elapsed().as_millis() + ); + respond(&mut client, b"HTTP/1.1 200 Connection Established\r\n\r\n").await?; // Check if endpoint has L7 config for protocol-aware inspection @@ -701,7 +717,9 @@ fn evaluate_opa_tcp( ); } + let total_start = std::time::Instant::now(); let peer_port = peer_addr.port(); + let (bin_path, binary_pid) = match crate::procfs::resolve_tcp_peer_identity(pid, peer_port) { Ok(r) => r, Err(e) => { @@ -732,7 +750,6 @@ fn evaluate_opa_tcp( // Walk the process tree upward to collect ancestor binaries let ancestors = crate::procfs::collect_ancestor_binaries(binary_pid, pid); - // TOFU verify each ancestor binary for ancestor in &ancestors { if let Err(e) = identity_cache.verify_or_cache(ancestor) { return deny( @@ -749,7 +766,6 @@ fn evaluate_opa_tcp( } // Collect cmdline paths for script-based binary detection. - // Excludes exe paths already captured in bin_path/ancestors to avoid duplicates. let mut exclude = ancestors.clone(); exclude.push(bin_path.clone()); let cmdline_paths = crate::procfs::collect_cmdline_paths(binary_pid, pid, &exclude); @@ -763,7 +779,7 @@ fn evaluate_opa_tcp( cmdline_paths: cmdline_paths.clone(), }; - match engine.evaluate_network_action(&input) { + let result = match engine.evaluate_network_action(&input) { Ok(action) => ConnectDecision { action, binary: Some(bin_path), @@ -778,7 +794,12 @@ fn evaluate_opa_tcp( ancestors, cmdline_paths, ), - } + }; + debug!( + "evaluate_opa_tcp TOTAL: {}ms host={host} port={port}", + total_start.elapsed().as_millis() + ); + result } /// Non-Linux stub: OPA identity binding requires /proc. @@ -1600,14 +1621,22 @@ async fn handle_forward_proxy( let peer_addr = client.peer_addr().into_diagnostic()?; let local_addr = client.local_addr().into_diagnostic()?; - let decision = evaluate_opa_tcp( - peer_addr, - &opa_engine, - &identity_cache, - &entrypoint_pid, - &host_lc, - port, - ); + let opa_clone = opa_engine.clone(); + let cache_clone = identity_cache.clone(); + let pid_clone = entrypoint_pid.clone(); + let host_clone = host_lc.clone(); + let decision = tokio::task::spawn_blocking(move || { + evaluate_opa_tcp( + peer_addr, + &opa_clone, + &cache_clone, + &pid_clone, + &host_clone, + port, + ) + }) + .await + .map_err(|e| miette::miette!("identity resolution task panicked: {e}"))?; // Build log context let binary_str = decision