Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ libc = "0.2"

[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2"
socket2 = "0.5"

[dev-dependencies]
tempfile = "3.8"
Expand Down
9 changes: 9 additions & 0 deletions src/jail/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<NetworkNamespace>::create(
Expand Down
20 changes: 16 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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;

Expand Down
61 changes: 53 additions & 8 deletions src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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<TcpListener> {
#[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<u16>,
https_port: Option<u16>,
Expand Down Expand Up @@ -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?;
Expand Down Expand Up @@ -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?;
Expand Down
Loading