Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a1e497b

Browse files
authored
linux: add namespace egress filter to block non-HTTP traffic (#11)
1 parent 6f3ca18 commit a1e497b

File tree

2 files changed

+62
-9
lines changed

2 files changed

+62
-9
lines changed

src/jail/linux/nftables.rs

Lines changed: 28 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,45 @@ 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+
# Explicitly block all other UDP (e.g., QUIC on 443)
164+
ip protocol udp drop
165+
166+
# Allow traffic to the host proxy ports after DNAT
167+
ip daddr {} tcp dport {{ {}, {} }} accept
168+
}}
150169
}}
151170
"#,
152-
table_name, host_ip, http_port, host_ip, https_port
171+
table_name, host_ip, http_port, host_ip, https_port, host_ip, http_port, https_port
153172
);
154173

155174
debug!(

tests/linux_integration.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,4 +280,38 @@ 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+
fn test_outbound_tcp_non_http_blocked() {
290+
LinuxPlatform::require_privileges();
291+
292+
// Attempt to connect to portquiz.net on port 81 (non-standard HTTP port)
293+
// Expectation: connection is blocked by namespace egress filter
294+
let mut cmd = httpjail_cmd();
295+
cmd.arg("-r").arg("allow: .*") // proxy allows HTTP/HTTPS, but port 81 should be blocked
296+
.arg("--")
297+
.arg("sh")
298+
.arg("-c")
299+
.arg("curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 --max-time 8 http://portquiz.net:81 && echo CONNECTED || echo BLOCKED");
300+
301+
let output = cmd.output().expect("Failed to execute httpjail");
302+
let stdout = String::from_utf8_lossy(&output.stdout);
303+
let stderr = String::from_utf8_lossy(&output.stderr);
304+
305+
eprintln!("[Linux] outbound TCP test stdout: {}", stdout);
306+
if !stderr.is_empty() {
307+
eprintln!("[Linux] outbound TCP test stderr: {}", stderr);
308+
}
309+
310+
assert!(
311+
stdout.contains("BLOCKED"),
312+
"Non-HTTP outbound TCP should be blocked. stdout: {}, stderr: {}",
313+
stdout.trim(),
314+
stderr.trim()
315+
);
316+
}
283317
}

0 commit comments

Comments
 (0)