Skip to content

Commit c773b9b

Browse files
kosaku-simclaude
andcommitted
feat: allow direct TCP 443 for OPENSHELL_DIRECT_TCP_HOSTS
Libraries like Node.js ws (used by @slack/socket-mode) resolve DNS then connect directly to the resolved IP on TCP 443, ignoring HTTP_PROXY. The sandbox iptables REJECT all bypass TCP, breaking these connections even after DNS resolution succeeds. Add OPENSHELL_DIRECT_TCP_HOSTS env var (comma-separated hostnames). At sandbox netns setup, resolve these hosts and install: - iptables ACCEPT for TCP 443 to resolved IPs (sandbox side) - MASQUERADE + FORWARD rules (host side) for return routing This pairs with the DNS ACCEPT rule from the previous commit to provide full direct connectivity for proxy-unaware libraries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7c7f4d5 commit c773b9b

File tree

1 file changed

+93
-0
lines changed
  • crates/openshell-sandbox/src/sandbox/linux

1 file changed

+93
-0
lines changed

crates/openshell-sandbox/src/sandbox/linux/netns.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,46 @@ const SUBNET_PREFIX: &str = "10.200.0";
1919
const HOST_IP_SUFFIX: u8 = 1;
2020
const SANDBOX_IP_SUFFIX: u8 = 2;
2121

22+
/// Resolve hostnames that require direct TCP access (bypassing the HTTP proxy).
23+
///
24+
/// Returns resolved IPv4 addresses for hosts listed in `OPENSHELL_DIRECT_TCP_HOSTS`
25+
/// (comma-separated hostnames). These hosts are resolved via the system DNS and
26+
/// get iptables ACCEPT rules for TCP port 443 in the sandbox netns, plus
27+
/// MASQUERADE on the host side so responses can return.
28+
///
29+
/// This is needed for libraries (e.g. Node.js `ws`) that make direct TCP
30+
/// connections after resolving DNS, ignoring HTTP_PROXY settings.
31+
fn resolve_direct_tcp_hosts() -> Vec<IpAddr> {
32+
let hosts = match std::env::var("OPENSHELL_DIRECT_TCP_HOSTS") {
33+
Ok(val) if !val.is_empty() => val,
34+
_ => return Vec::new(),
35+
};
36+
37+
let mut addrs = Vec::new();
38+
for host in hosts.split(',') {
39+
let host = host.trim();
40+
if host.is_empty() {
41+
continue;
42+
}
43+
// Use std::net to resolve — this runs in the pod netns (not sandbox)
44+
// so cluster DNS works normally.
45+
match std::net::ToSocketAddrs::to_socket_addrs(&(host, 443_u16)) {
46+
Ok(iter) => {
47+
for sa in iter {
48+
if sa.is_ipv4() && !addrs.contains(&sa.ip()) {
49+
addrs.push(sa.ip());
50+
}
51+
}
52+
info!(host = %host, count = addrs.len(), "Resolved direct TCP host");
53+
}
54+
Err(e) => {
55+
warn!(host = %host, error = %e, "Failed to resolve direct TCP host");
56+
}
57+
}
58+
}
59+
addrs
60+
}
61+
2262
/// Resolve the cluster DNS server IP for the iptables ACCEPT rule.
2363
///
2464
/// Priority:
@@ -389,6 +429,36 @@ impl NetworkNamespace {
389429
);
390430
}
391431

432+
// Host-side forwarding for direct TCP 443 (OPENSHELL_DIRECT_TCP_HOSTS).
433+
// Same pattern as DNS: MASQUERADE so the destination sees the pod IP.
434+
let direct_tcp_ips = resolve_direct_tcp_hosts();
435+
if !direct_tcp_ips.is_empty() {
436+
let sandbox_cidr = format!("{}/32", self.sandbox_ip);
437+
for ip in &direct_tcp_ips {
438+
let ip_cidr = format!("{ip}/32");
439+
let _ = Command::new(&iptables_path)
440+
.args([
441+
"-t", "nat", "-A", "POSTROUTING",
442+
"-s", &sandbox_cidr, "-d", &ip_cidr,
443+
"-p", "tcp", "--dport", "443",
444+
"-j", "MASQUERADE",
445+
])
446+
.output();
447+
let _ = Command::new(&iptables_path)
448+
.args([
449+
"-A", "FORWARD",
450+
"-s", &sandbox_cidr, "-d", &ip_cidr,
451+
"-p", "tcp", "--dport", "443",
452+
"-j", "ACCEPT",
453+
])
454+
.output();
455+
}
456+
info!(
457+
count = direct_tcp_ips.len(),
458+
"Enabled direct TCP 443 forwarding for OPENSHELL_DIRECT_TCP_HOSTS"
459+
);
460+
}
461+
392462
info!(
393463
namespace = %self.name,
394464
"Bypass detection rules installed"
@@ -477,6 +547,29 @@ impl NetworkNamespace {
477547
);
478548
}
479549

550+
// Rule 4.5: ACCEPT direct TCP 443 to hosts listed in OPENSHELL_DIRECT_TCP_HOSTS.
551+
//
552+
// Some libraries (e.g. Node.js `ws`, used by @slack/socket-mode) resolve
553+
// DNS and then connect directly to the resolved IP, ignoring HTTP_PROXY.
554+
// For these hosts, allow TCP 443 through and rely on host-side MASQUERADE
555+
// (set up in install_bypass_rules) to route the traffic.
556+
for direct_ip in resolve_direct_tcp_hosts() {
557+
let ip_cidr = format!("{direct_ip}/32");
558+
if let Err(e) = run_iptables_netns(
559+
&self.name,
560+
iptables_cmd,
561+
&[
562+
"-A", "OUTPUT", "-d", &ip_cidr, "-p", "tcp", "--dport", "443", "-j", "ACCEPT",
563+
],
564+
) {
565+
warn!(
566+
error = %e,
567+
ip = %direct_ip,
568+
"Failed to install direct TCP ACCEPT rule"
569+
);
570+
}
571+
}
572+
480573
// Rule 5: REJECT TCP bypass attempts (fast-fail)
481574
run_iptables_netns(
482575
&self.name,

0 commit comments

Comments
 (0)