Skip to content

Commit b3d5052

Browse files
committed
linux: add namespace egress filter to block non-HTTP TCP and add integration test using portquiz.net
1 parent 6f3ca18 commit b3d5052

2 files changed

Lines changed: 60 additions & 9 deletions

File tree

src/jail/linux/nftables.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ table ip {} {{
121121
})
122122
}
123123

124-
/// Create namespace-side nftables rules for traffic redirection
124+
/// Create namespace-side nftables rules for traffic redirection and egress filtering
125125
pub fn new_namespace_table(
126126
namespace: &str,
127127
host_ip: &str,
@@ -130,26 +130,42 @@ table ip {} {{
130130
) -> Result<Self> {
131131
let table_name = "httpjail".to_string();
132132

133-
// Generate the ruleset for namespace-side DNAT
133+
// Generate the ruleset for namespace-side DNAT + FILTER
134134
let ruleset = format!(
135135
r#"
136136
table ip {} {{
137+
# NAT output chain: redirect HTTP/HTTPS to host proxy
137138
chain output {{
138139
type nat hook output priority -100; policy accept;
139-
140-
# Skip DNS traffic
140+
141+
# Skip DNS traffic from NAT processing
141142
udp dport 53 return
142143
tcp dport 53 return
143-
144-
# Redirect HTTP to proxy
144+
145+
# Redirect HTTP to proxy running on host
145146
tcp dport 80 dnat to {}:{}
146-
147-
# Redirect HTTPS to proxy
147+
148+
# Redirect HTTPS to proxy running on host
148149
tcp dport 443 dnat to {}:{}
149150
}}
151+
152+
# FILTER output chain: block non-HTTP/HTTPS egress
153+
chain outfilter {{
154+
type filter hook output priority 0; policy drop;
155+
156+
# Always allow established/related traffic
157+
ct state established,related accept
158+
159+
# Allow DNS to anywhere
160+
udp dport 53 accept
161+
tcp dport 53 accept
162+
163+
# Allow traffic to the host proxy ports after DNAT
164+
ip daddr {} tcp dport {{ {}, {} }} accept
165+
}}
150166
}}
151167
"#,
152-
table_name, host_ip, http_port, host_ip, https_port
168+
table_name, host_ip, http_port, host_ip, https_port, host_ip, http_port, https_port
153169
);
154170

155171
debug!(

tests/linux_integration.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,4 +280,39 @@ mod tests {
280280
initial_ns_count, final_ns_count
281281
);
282282
}
283+
284+
/// Verify outbound TCP connections to non-HTTP ports are blocked inside the jail
285+
///
286+
/// Uses portquiz.net which listens on all TCP ports and returns an HTTP response,
287+
/// allowing us to test egress on non-standard ports reliably.
288+
#[test]
289+
#[serial]
290+
fn test_outbound_tcp_non_http_blocked() {
291+
LinuxPlatform::require_privileges();
292+
293+
// Attempt to connect to portquiz.net on port 81 (non-standard HTTP port)
294+
// Expectation: connection is blocked by namespace egress filter
295+
let mut cmd = httpjail_cmd();
296+
cmd.arg("-r").arg("allow: .*") // proxy allows HTTP/HTTPS, but port 81 should be blocked
297+
.arg("--")
298+
.arg("sh")
299+
.arg("-c")
300+
.arg("curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 --max-time 8 http://portquiz.net:81 && echo CONNECTED || echo BLOCKED");
301+
302+
let output = cmd.output().expect("Failed to execute httpjail");
303+
let stdout = String::from_utf8_lossy(&output.stdout);
304+
let stderr = String::from_utf8_lossy(&output.stderr);
305+
306+
eprintln!("[Linux] outbound TCP test stdout: {}", stdout);
307+
if !stderr.is_empty() {
308+
eprintln!("[Linux] outbound TCP test stderr: {}", stderr);
309+
}
310+
311+
assert!(
312+
stdout.contains("BLOCKED"),
313+
"Non-HTTP outbound TCP should be blocked. stdout: {}, stderr: {}",
314+
stdout.trim(),
315+
stderr.trim()
316+
);
317+
}
283318
}

0 commit comments

Comments
 (0)