From b3d5052c33a0832ce1be22e9938b8f94e224c907 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Wed, 10 Sep 2025 12:04:33 -0500 Subject: [PATCH 1/4] linux: add namespace egress filter to block non-HTTP TCP and add integration test using portquiz.net --- src/jail/linux/nftables.rs | 34 +++++++++++++++++++++++++--------- tests/linux_integration.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/jail/linux/nftables.rs b/src/jail/linux/nftables.rs index 9d4dc41f..2dcd4976 100644 --- a/src/jail/linux/nftables.rs +++ b/src/jail/linux/nftables.rs @@ -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, @@ -130,26 +130,42 @@ table ip {} {{ ) -> Result { 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 + + # 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!( diff --git a/tests/linux_integration.rs b/tests/linux_integration.rs index 398e3be4..c50743f8 100644 --- a/tests/linux_integration.rs +++ b/tests/linux_integration.rs @@ -280,4 +280,39 @@ 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] + #[serial] + 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() + ); + } } From 13e10788cebd06fbe6ee01906b2032c1d00924ff Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Wed, 10 Sep 2025 12:06:18 -0500 Subject: [PATCH 2/4] linux: explicitly drop all UDP egress except DNS in namespace filter --- src/jail/linux/nftables.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/jail/linux/nftables.rs b/src/jail/linux/nftables.rs index 2dcd4976..76a9458d 100644 --- a/src/jail/linux/nftables.rs +++ b/src/jail/linux/nftables.rs @@ -160,6 +160,9 @@ table ip {} {{ udp dport 53 accept tcp dport 53 accept + # Explicitly block all other UDP (e.g., QUIC on 443) + udp drop + # Allow traffic to the host proxy ports after DNAT ip daddr {} tcp dport {{ {}, {} }} accept }} From 77f6caa20c8cd741d31aae419973e4add92be624 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Wed, 10 Sep 2025 12:09:45 -0500 Subject: [PATCH 3/4] linux: fix nftables syntax (use 'ip protocol udp drop') --- src/jail/linux/nftables.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jail/linux/nftables.rs b/src/jail/linux/nftables.rs index 76a9458d..64f82a43 100644 --- a/src/jail/linux/nftables.rs +++ b/src/jail/linux/nftables.rs @@ -161,7 +161,7 @@ table ip {} {{ tcp dport 53 accept # Explicitly block all other UDP (e.g., QUIC on 443) - udp drop + ip protocol udp drop # Allow traffic to the host proxy ports after DNAT ip daddr {} tcp dport {{ {}, {} }} accept From f7b137447f9164bc9d06e5e29f6bd60a2d588fda Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Wed, 10 Sep 2025 12:12:20 -0500 Subject: [PATCH 4/4] tests(linux): remove unnecessary #[serial] from outbound TCP block test --- tests/linux_integration.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/linux_integration.rs b/tests/linux_integration.rs index c50743f8..1235649e 100644 --- a/tests/linux_integration.rs +++ b/tests/linux_integration.rs @@ -286,7 +286,6 @@ mod tests { /// 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] - #[serial] fn test_outbound_tcp_non_http_blocked() { LinuxPlatform::require_privileges();