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..635f90bf 100644 --- a/src/jail/linux/mod.rs +++ b/src/jail/linux/mod.rs @@ -145,6 +145,15 @@ 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( diff --git a/src/main.rs b/src/main.rs index 23619e21..bb3fa827 100644 --- a/src/main.rs +++ b/src/main.rs @@ -282,6 +282,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( @@ -418,11 +421,21 @@ 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")] + { + Some( + httpjail::jail::linux::LinuxJail::compute_host_ip_for_jail_id(&jail_config.jail_id), + ) + } + #[cfg(not(target_os = "linux"))] + { + 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 @@ -441,7 +454,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; diff --git a/src/proxy.rs b/src/proxy.rs index 0e0add21..aa993c57 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -14,6 +14,15 @@ use hyper_rustls::HttpsConnectorBuilder; use hyper_util::client::legacy::Client; use hyper_util::rt::{TokioExecutor, TokioIo}; use rand::Rng; + +#[cfg(target_os = "linux")] +use std::os::fd::AsRawFd; + +#[cfg(target_os = "linux")] +use socket2::{Domain, Protocol, Socket, Type}; + +#[cfg(target_os = "linux")] +use std::net::Ipv4Addr; use std::net::SocketAddr; use std::sync::{Arc, OnceLock}; use std::time::Duration; @@ -171,7 +180,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); @@ -186,6 +195,47 @@ 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, @@ -217,11 +267,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?; @@ -263,9 +310,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?;