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
37 changes: 28 additions & 9 deletions src/jail/linux/nftables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ table ip {} {{
})
}

/// Create namespace-side nftables rules for traffic redirection
/// Create namespace-side nftables rules for traffic redirection and egress filtering
pub fn new_namespace_table(
namespace: &str,
host_ip: &str,
Expand All @@ -130,26 +130,45 @@ table ip {} {{
) -> Result<Self> {
let table_name = "httpjail".to_string();

// Generate the ruleset for namespace-side DNAT
// Generate the ruleset for namespace-side DNAT + FILTER
let ruleset = format!(
r#"
table ip {} {{
# NAT output chain: redirect HTTP/HTTPS to host proxy
chain output {{
type nat hook output priority -100; policy accept;
# Skip DNS traffic

# Skip DNS traffic from NAT processing
udp dport 53 return
tcp dport 53 return
# Redirect HTTP to proxy

# Redirect HTTP to proxy running on host
tcp dport 80 dnat to {}:{}
# Redirect HTTPS to proxy

# Redirect HTTPS to proxy running on host
tcp dport 443 dnat to {}:{}
}}

# FILTER output chain: block non-HTTP/HTTPS egress
chain outfilter {{
type filter hook output priority 0; policy drop;

# Always allow established/related traffic
ct state established,related accept

# Allow DNS to anywhere
udp dport 53 accept
tcp dport 53 accept

# Explicitly block all other UDP (e.g., QUIC on 443)
ip protocol udp drop

# Allow traffic to the host proxy ports after DNAT
ip daddr {} tcp dport {{ {}, {} }} accept
}}
}}
"#,
table_name, host_ip, http_port, host_ip, https_port
table_name, host_ip, http_port, host_ip, https_port, host_ip, http_port, https_port
);

debug!(
Expand Down
34 changes: 34 additions & 0 deletions tests/linux_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,38 @@ mod tests {
initial_ns_count, final_ns_count
);
}

/// Verify outbound TCP connections to non-HTTP ports are blocked inside the jail
///
/// Uses portquiz.net which listens on all TCP ports and returns an HTTP response,
/// allowing us to test egress on non-standard ports reliably.
#[test]
fn test_outbound_tcp_non_http_blocked() {
LinuxPlatform::require_privileges();

// Attempt to connect to portquiz.net on port 81 (non-standard HTTP port)
// Expectation: connection is blocked by namespace egress filter
let mut cmd = httpjail_cmd();
cmd.arg("-r").arg("allow: .*") // proxy allows HTTP/HTTPS, but port 81 should be blocked
.arg("--")
.arg("sh")
.arg("-c")
.arg("curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 --max-time 8 http://portquiz.net:81 && echo CONNECTED || echo BLOCKED");

let output = cmd.output().expect("Failed to execute httpjail");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);

eprintln!("[Linux] outbound TCP test stdout: {}", stdout);
if !stderr.is_empty() {
eprintln!("[Linux] outbound TCP test stderr: {}", stderr);
}

assert!(
stdout.contains("BLOCKED"),
"Non-HTTP outbound TCP should be blocked. stdout: {}, stderr: {}",
stdout.trim(),
stderr.trim()
);
}
}