From 940f7f0b810da768623d7f5b0e5f215ef1364637 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 20:30:56 +0000 Subject: [PATCH 1/3] Security: bind proxy to jail veth host IP in strong mode (fix #31)\n\n- main: create JailConfig early and bind proxy to per-jail veth host IP on Linux\n- jail/linux: expose LinuxJail::compute_host_ip_for_jail_id(jail_id)\n- proxy: bind using IP_FREEBIND on Linux so we can pre-bind before interface exists\n- cargo: add socket2 (linux only)\n\nThis removes listening on 0.0.0.0 in strong jail mode to avoid exposing proxy ports.\n\nCo-authored-by: ammario <7416144+ammario@users.noreply.github.com> --- Cargo.lock | 15 ++++++++++++-- Cargo.toml | 1 + src/jail/linux/mod.rs | 10 +++++++++- src/main.rs | 26 +++++++++++++++++++----- src/proxy.rs | 46 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 89 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a128b5f6..57c67d89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -797,6 +797,7 @@ dependencies = [ "rcgen", "rustls", "serial_test", + "socket2 0.5.10", "tempfile", "tls-parser", "tokio", @@ -867,7 +868,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.0", "system-configuration", "tokio", "tower-service", @@ -1885,6 +1886,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.0" @@ -2073,7 +2084,7 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2", + "socket2 0.6.0", "tokio-macros", "windows-sys 0.59.0", ] diff --git a/Cargo.toml b/Cargo.toml index 47e62b1b..6064a934 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ libc = "0.2" [target.'cfg(target_os = "linux")'.dependencies] libc = "0.2" +socket2 = "0.5" [dev-dependencies] tempfile = "3.8" diff --git a/src/jail/linux/mod.rs b/src/jail/linux/mod.rs index 1181a89d..224222dc 100644 --- a/src/jail/linux/mod.rs +++ b/src/jail/linux/mod.rs @@ -145,6 +145,14 @@ impl LinuxJail { (host_ip, host_cidr, guest_cidr, subnet_cidr) } + /// Expose the host veth IPv4 address for a given jail_id. + /// This allows early binding of the proxy to the precise interface IP + /// without falling back to 0.0.0.0. + pub fn compute_host_ip_for_jail_id(jail_id: &str) -> [u8; 4] { + let (host_ip, _host_cidr, _guest_cidr, _subnet_cidr) = Self::compute_subnet_for_jail(jail_id); + host_ip + } + /// Create the network namespace using ManagedResource fn create_namespace(&mut self) -> Result<()> { self.namespace = Some(ManagedResource::::create( @@ -759,4 +767,4 @@ impl Clone for LinuxJail { subnet_cidr: self.subnet_cidr.clone(), } } -} +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 29a01ce8..0e5daaac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -272,6 +272,9 @@ async fn main() -> Result<()> { info!("Starting httpjail in server mode"); } + // Initialize jail configuration early to allow computing the host IP + let mut jail_config = JailConfig::new(); + // Build rule engine based on script or JS let request_log = if let Some(path) = &args.request_log { Some(Arc::new(Mutex::new( @@ -358,11 +361,25 @@ async fn main() -> Result<()> { // so the proxy is accessible from the veth interface. For weak mode or server mode, // localhost is fine. // TODO: This has security implications - see GitHub issue #31 - let bind_address = if args.weak || args.server { - None // defaults to 127.0.0.1 + let bind_address: Option<[u8; 4]> = if args.weak || args.server { + None } else { - Some([0, 0, 0, 0]) // bind to all interfaces for strong jail + #[cfg(target_os = "linux")] + { + // Compute veth host IP for jail_id + let host_ip = Some(httpjail::jail::linux::LinuxJail::compute_host_ip_for_jail_id( + &jail_config.jail_id, + )); + host_ip // Return the host IP + } + + #[cfg(not(target_os = "linux") )] + { + // Fallback bind address for non-Linux systems + None + } }; + let mut proxy = ProxyServer::new(http_port, https_port, rule_engine, bind_address); // Start proxy in background if running as server; otherwise start with random ports @@ -381,7 +398,6 @@ async fn main() -> Result<()> { std::fs::create_dir_all("/tmp/httpjail").ok(); // Configure and execute the target command inside a jail - let mut jail_config = JailConfig::new(); jail_config.http_proxy_port = actual_http_port; jail_config.https_proxy_port = actual_https_port; @@ -477,4 +493,4 @@ async fn main() -> Result<()> { } Ok(()) -} +} \ No newline at end of file diff --git a/src/proxy.rs b/src/proxy.rs index 0e0add21..0155293c 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -14,7 +14,14 @@ use hyper_rustls::HttpsConnectorBuilder; use hyper_util::client::legacy::Client; use hyper_util::rt::{TokioExecutor, TokioIo}; use rand::Rng; -use std::net::SocketAddr; + +#[cfg(target_os = "linux")] +use std::os::fd::AsRawFd; + +#[cfg(target_os = "linux")] +use socket2::{Domain, Protocol, Socket, Type}; + +use std::net::{Ipv4Addr, SocketAddr}; use std::sync::{Arc, OnceLock}; use std::time::Duration; use tokio::net::{TcpListener, TcpStream}; @@ -186,6 +193,43 @@ async fn bind_to_available_port(start: u16, end: u16, bind_addr: [u8; 4]) -> Res ) } +async fn bind_ipv4_listener(bind_addr: [u8; 4], port: u16) -> Result { + #[cfg(target_os = "linux")] + { + // Setup a raw socket to set IP_FREEBIND for specific non-loopback addresses + let ip = Ipv4Addr::from(bind_addr); + let is_specific_non_loopback = ip != Ipv4Addr::new(127, 0, 0, 1) && ip != Ipv4Addr::new(0, 0, 0, 0); + if is_specific_non_loopback { + let sock = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?; + // Enabling FREEBIND for non-local address binding before interface configuration + unsafe { + let yes: libc::c_int = 1; + let ret = libc::setsockopt( + sock.as_raw_fd(), + libc::IPPROTO_IP, + libc::IP_FREEBIND, + &yes as *const _ as *const libc::c_void, + std::mem::size_of_val(&yes) as libc::socklen_t, + ); + if ret != 0 { + warn!("Failed to set IP_FREEBIND on socket: errno={} (continuing)", ret); + } + } + + sock.set_nonblocking(true)?; + let addr = SocketAddr::from((ip, port)); + sock.bind(&addr.into())?; + sock.listen(1024)?; // OS default backlog + let std_listener: std::net::TcpListener = sock.into(); + std_listener.set_nonblocking(true)?; + return Ok(TcpListener::from_std(std_listener)?); + } + } + // Fallback: normal async bind if the conditions aren't met + let listener = TcpListener::bind(SocketAddr::from((bind_addr, port))).await?; + Ok(listener) +} + pub struct ProxyServer { http_port: Option, https_port: Option, From fb1d2968a9b955bf2151ace13c940891d140ae07 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 20:40:45 +0000 Subject: [PATCH 2/3] proxy: use bind_ipv4_listener (IP_FREEBIND) for all binds; fix clippy cfg blocks in main.rs\n\nThis ensures strong-mode binds succeed before veth IP exists and satisfies clippy.\n\nCo-authored-by: ammario <7416144+ammario@users.noreply.github.com> --- src/main.rs | 10 +++------- src/proxy.rs | 11 +++-------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0e5daaac..c4301b6a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -366,16 +366,12 @@ async fn main() -> Result<()> { } else { #[cfg(target_os = "linux")] { - // Compute veth host IP for jail_id - let host_ip = Some(httpjail::jail::linux::LinuxJail::compute_host_ip_for_jail_id( + Some(httpjail::jail::linux::LinuxJail::compute_host_ip_for_jail_id( &jail_config.jail_id, - )); - host_ip // Return the host IP + )) } - - #[cfg(not(target_os = "linux") )] + #[cfg(not(target_os = "linux"))] { - // Fallback bind address for non-Linux systems None } }; diff --git a/src/proxy.rs b/src/proxy.rs index 0155293c..78804c22 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -178,7 +178,7 @@ async fn bind_to_available_port(start: u16, end: u16, bind_addr: [u8; 4]) -> Res for _ in 0..16 { let port = rng.gen_range(start..=end); - match TcpListener::bind(SocketAddr::from((bind_addr, port))).await { + match bind_ipv4_listener(bind_addr, port).await { Ok(listener) => { debug!("Successfully bound to port {}", port); return Ok(listener); @@ -261,11 +261,8 @@ impl ProxyServer { } pub async fn start(&mut self) -> Result<(u16, u16)> { - // Start HTTP proxy let http_listener = if let Some(port) = self.http_port { - // If port is 0, let OS choose any available port - // Otherwise bind to the specified port - TcpListener::bind(SocketAddr::from((self.bind_address, port))).await? + bind_ipv4_listener(self.bind_address, port).await? } else { // No port specified, find available port in 8000-8999 range let listener = bind_to_available_port(8000, 8999, self.bind_address).await?; @@ -307,9 +304,7 @@ impl ProxyServer { // Start HTTPS proxy let https_listener = if let Some(port) = self.https_port { - // If port is 0, let OS choose any available port - // Otherwise bind to the specified port - TcpListener::bind(SocketAddr::from((self.bind_address, port))).await? + bind_ipv4_listener(self.bind_address, port).await? } else { // No port specified, find available port in 8000-8999 range let listener = bind_to_available_port(8000, 8999, self.bind_address).await?; From ee4611e90510e409f2686f3f49be6970f9928207 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Thu, 11 Sep 2025 15:53:10 -0500 Subject: [PATCH 3/3] fix: resolve formatting and clippy warnings on macOS - Apply cargo fmt to fix formatting issues - Move Ipv4Addr import to Linux-only scope to fix unused import warning on macOS --- src/jail/linux/mod.rs | 5 +++-- src/main.rs | 8 ++++---- src/proxy.rs | 16 +++++++++++----- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/jail/linux/mod.rs b/src/jail/linux/mod.rs index 224222dc..635f90bf 100644 --- a/src/jail/linux/mod.rs +++ b/src/jail/linux/mod.rs @@ -149,7 +149,8 @@ impl LinuxJail { /// This allows early binding of the proxy to the precise interface IP /// without falling back to 0.0.0.0. pub fn compute_host_ip_for_jail_id(jail_id: &str) -> [u8; 4] { - let (host_ip, _host_cidr, _guest_cidr, _subnet_cidr) = Self::compute_subnet_for_jail(jail_id); + let (host_ip, _host_cidr, _guest_cidr, _subnet_cidr) = + Self::compute_subnet_for_jail(jail_id); host_ip } @@ -767,4 +768,4 @@ impl Clone for LinuxJail { subnet_cidr: self.subnet_cidr.clone(), } } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index c4301b6a..9bcda6cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -366,9 +366,9 @@ async fn main() -> Result<()> { } else { #[cfg(target_os = "linux")] { - Some(httpjail::jail::linux::LinuxJail::compute_host_ip_for_jail_id( - &jail_config.jail_id, - )) + Some( + httpjail::jail::linux::LinuxJail::compute_host_ip_for_jail_id(&jail_config.jail_id), + ) } #[cfg(not(target_os = "linux"))] { @@ -489,4 +489,4 @@ async fn main() -> Result<()> { } Ok(()) -} \ No newline at end of file +} diff --git a/src/proxy.rs b/src/proxy.rs index 78804c22..aa993c57 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -21,7 +21,9 @@ use std::os::fd::AsRawFd; #[cfg(target_os = "linux")] use socket2::{Domain, Protocol, Socket, Type}; -use std::net::{Ipv4Addr, SocketAddr}; +#[cfg(target_os = "linux")] +use std::net::Ipv4Addr; +use std::net::SocketAddr; use std::sync::{Arc, OnceLock}; use std::time::Duration; use tokio::net::{TcpListener, TcpStream}; @@ -198,7 +200,8 @@ async fn bind_ipv4_listener(bind_addr: [u8; 4], port: u16) -> Result Result