Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 50 additions & 22 deletions crates/openshell-sandbox/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,12 +347,12 @@ async fn handle_tcp_connection(
);

// Extract action string and matched policy for logging
let (action_str, matched_policy, deny_reason) = match &decision.action {
NetworkAction::Allow { matched_policy } => ("allow", matched_policy.clone(), String::new()),
NetworkAction::Deny { reason } => ("deny", None, reason.clone()),
let (matched_policy, deny_reason) = match &decision.action {
NetworkAction::Allow { matched_policy } => (matched_policy.clone(), String::new()),
NetworkAction::Deny { reason } => (None, reason.clone()),
};

// Unified log line: one info! per CONNECT with full context
// Build log context fields (shared by deny log below and deferred allow log after L7 check)
let binary_str = decision
.binary
.as_ref()
Expand Down Expand Up @@ -382,24 +382,26 @@ async fn handle_tcp_connection(
};
let policy_str = matched_policy.as_deref().unwrap_or("-");

info!(
src_addr = %peer_addr.ip(),
src_port = peer_addr.port(),
proxy_addr = %local_addr,
dst_host = %host_lc,
dst_port = port,
binary = %binary_str,
binary_pid = %pid_str,
ancestors = %ancestors_str,
cmdline = %cmdline_str,
action = %action_str,
engine = "opa",
policy = %policy_str,
reason = %deny_reason,
"CONNECT",
);

// Log denied connections immediately — they never reach L7.
// Allowed connections are logged after the L7 config check (below)
// so we can distinguish CONNECT (L4-only) from CONNECT_L7 (L7 follows).
if matches!(decision.action, NetworkAction::Deny { .. }) {
info!(
src_addr = %peer_addr.ip(),
src_port = peer_addr.port(),
proxy_addr = %local_addr,
dst_host = %host_lc,
dst_port = port,
binary = %binary_str,
binary_pid = %pid_str,
ancestors = %ancestors_str,
cmdline = %cmdline_str,
action = "deny",
engine = "opa",
policy = "-",
reason = %deny_reason,
"CONNECT",
);
emit_denial(
&denial_tx,
&host_lc,
Expand Down Expand Up @@ -498,7 +500,33 @@ async fn handle_tcp_connection(
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
if let Some(l7_config) = query_l7_config(&opa_engine, &decision, &host_lc, port) {
let l7_config = query_l7_config(&opa_engine, &decision, &host_lc, port);

// Log the allowed CONNECT — use CONNECT_L7 when L7 inspection follows,
// so log consumers can distinguish L4-only decisions from tunnel lifecycle events.
let connect_msg = if l7_config.is_some() {
"CONNECT_L7"
} else {
"CONNECT"
};
info!(
src_addr = %peer_addr.ip(),
src_port = peer_addr.port(),
proxy_addr = %local_addr,
dst_host = %host_lc,
dst_port = port,
binary = %binary_str,
binary_pid = %pid_str,
ancestors = %ancestors_str,
cmdline = %cmdline_str,
action = "allow",
engine = "opa",
policy = %policy_str,
reason = "",
connect_msg,
);

if let Some(l7_config) = l7_config {
// Clone engine for per-tunnel L7 evaluation (cheap: shares compiled policy via Arc)
let tunnel_engine = opa_engine.clone_engine_for_tunnel().unwrap_or_else(|e| {
warn!(error = %e, "Failed to clone OPA engine for L7, falling back to L4-only");
Expand Down
20 changes: 20 additions & 0 deletions crates/openshell-tui/src/ui/sandbox_logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ const L7_FIELD_ORDER: &[&str] = &[

/// Return fields in a smart order based on the log message type.
pub(crate) fn ordered_fields<'a>(log: &'a LogLine) -> Vec<(&'a str, &'a str)> {
// Matches both "CONNECT" (L4-only decision) and "CONNECT_L7" (tunnel lifecycle for L7 endpoints)
let order: Option<&[&str]> = if log.message.starts_with("CONNECT") {
Some(CONNECT_FIELD_ORDER)
} else if log.message.starts_with("L7_REQUEST") {
Expand Down Expand Up @@ -517,6 +518,25 @@ mod tests {
assert!(dst_pos < binary_pos);
}

#[test]
fn plain_format_connect_l7_field_order() {
let log = make_log(
"CONNECT_L7",
vec![
("binary", "/usr/bin/curl"),
("action", "allow"),
("dst_host", "api.example.com"),
],
);
let result = format_log_line_plain(&log);
// CONNECT_L7 should use the same field ordering as CONNECT
let action_pos = result.find("action=").unwrap();
let dst_pos = result.find("dst_host=").unwrap();
let binary_pos = result.find("binary=").unwrap();
assert!(action_pos < dst_pos);
assert!(dst_pos < binary_pos);
}

#[test]
fn plain_format_l7_field_order() {
let log = make_log(
Expand Down
Loading