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
130 changes: 95 additions & 35 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,12 @@ async fn main() -> Result<()> {
}
}

setup_logging(args.run_args.verbose);
// Server mode defaults to INFO level logging for visibility
let verbosity = args
.run_args
.verbose
.max(if args.run_args.server { 1 } else { 0 });
setup_logging(verbosity);

// Log the version at startup for easier diagnostics
debug!("httpjail version: {}", env!("VERSION_WITH_GIT_HASH"));
Expand Down Expand Up @@ -487,64 +492,119 @@ async fn main() -> Result<()> {
}

// Parse bind configuration from env vars
// Supports both "port" and "ip:port" formats
fn parse_bind_config(env_var: &str) -> (Option<u16>, Option<std::net::IpAddr>) {
// Returns Some(addr) for "port", ":port", or "ip:port" formats (including explicit :0)
// Returns None for "ip" only or missing config
fn parse_bind_config(env_var: &str) -> Option<std::net::SocketAddr> {
if let Ok(val) = std::env::var(env_var) {
if let Some(colon_pos) = val.rfind(':') {
// Try to parse as ip:port
let ip_str = &val[..colon_pos];
let port_str = &val[colon_pos + 1..];
match port_str.parse::<u16>() {
Ok(port) => match ip_str.parse::<std::net::IpAddr>() {
Ok(ip) => (Some(port), Some(ip)),
Err(_) => (Some(port), None),
},
Err(_) => (None, None),
}
} else {
// Try to parse as port
match val.parse::<u16>() {
Ok(port) => (Some(port), None),
Err(_) => (None, None),
let val = val.trim();

// First try parsing as "ip:port" (respects explicit :0)
if let Ok(addr) = val.parse::<std::net::SocketAddr>() {
return Some(addr);
}

// Try parsing as ":port" (Go-style) - bind to all interfaces (0.0.0.0)
if let Some(port_str) = val.strip_prefix(':')
&& let Ok(port) = port_str.parse::<u16>()
{
return Some(std::net::SocketAddr::from(([0, 0, 0, 0], port)));
}

// Try parsing as just a port number - bind to all interfaces (0.0.0.0)
if let Ok(port) = val.parse::<u16>() {
return Some(std::net::SocketAddr::from(([0, 0, 0, 0], port)));
Comment on lines +513 to +515

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Port-only bind exposes server on all interfaces

When the bind environment variables contain only a port, the new parser builds a SocketAddr with 0.0.0.0, so HTTPJAIL_HTTP_BIND=8080 now listens on every interface. Previously such configuration bound to loopback, and the commit summary still claims that the port-only format defaults to localhost. This change silently broadens the listener’s exposure and can unintentionally publish the proxy to the whole network. It would be safer to keep the default IP at 127.0.0.1 and only bind to 0.0.0.0 when the user explicitly supplies an address.

Useful? React with 👍 / 👎.

}
}
None
}

// Parse IP-only from env var (for default port handling)
fn parse_ip_from_env(env_var: &str) -> Option<std::net::IpAddr> {
std::env::var(env_var).ok()?.parse().ok()
}

// Resolve bind address with optional default port for IP-only configs
fn resolve_bind_with_default(
parsed: Option<std::net::SocketAddr>,
env_var: &str,
default_ip: std::net::IpAddr,
default_port: u16,
) -> Option<std::net::SocketAddr> {
match parsed {
Some(addr) => Some(addr), // Respect explicit config including :0
None => {
// Check if user provided just IP without port
if let Some(ip) = parse_ip_from_env(env_var) {
Some(std::net::SocketAddr::new(ip, default_port))
} else {
Some(std::net::SocketAddr::new(default_ip, default_port))
}
}
} else {
(None, None)
}
}

// Determine ports to bind
let (http_port, _http_ip) = parse_bind_config("HTTPJAIL_HTTP_BIND");
let (https_port, _https_ip) = parse_bind_config("HTTPJAIL_HTTPS_BIND");
// Determine bind addresses
let http_bind = parse_bind_config("HTTPJAIL_HTTP_BIND");
let https_bind = parse_bind_config("HTTPJAIL_HTTPS_BIND");

// For strong jail mode (not weak, not server), we need to bind to all interfaces (0.0.0.0)
// For strong jail mode (not weak, not server), we need to bind to a specific IP
// so the proxy is accessible from the veth interface. For weak mode or server mode,
// localhost is fine.
// use the configured address or defaults.
// TODO: This has security implications - see GitHub issue #31
let bind_address: Option<[u8; 4]> = if args.run_args.weak || args.run_args.server {
None
let (http_bind, https_bind) = if args.run_args.server {
// Server mode: default to localhost:8080/8443, respect explicit ports including :0
let localhost = std::net::IpAddr::from([127, 0, 0, 1]);
let http = resolve_bind_with_default(http_bind, "HTTPJAIL_HTTP_BIND", localhost, 8080);
let https = resolve_bind_with_default(https_bind, "HTTPJAIL_HTTPS_BIND", localhost, 8443);
(http, https)
} else if args.run_args.weak {
// Weak mode: If IP-only provided, use port 0 (OS auto-select), else None
let http = http_bind.or_else(|| {
parse_ip_from_env("HTTPJAIL_HTTP_BIND").map(|ip| std::net::SocketAddr::new(ip, 0))
});
let https = https_bind.or_else(|| {
parse_ip_from_env("HTTPJAIL_HTTPS_BIND").map(|ip| std::net::SocketAddr::new(ip, 0))
});
(http, https)
} else {
#[cfg(target_os = "linux")]
{
Some(
httpjail::jail::linux::LinuxJail::compute_host_ip_for_jail_id(&jail_config.jail_id),
)
let jail_ip =
httpjail::jail::linux::LinuxJail::compute_host_ip_for_jail_id(&jail_config.jail_id);
// For strong jail mode, we need to bind to the jail IP.
// Use env var port if provided, otherwise use port 0 (auto-select) on jail IP.
let http_addr = match http_bind {
Some(addr) => std::net::SocketAddr::from((jail_ip, addr.port())),
None => std::net::SocketAddr::from((jail_ip, 0)), // Port 0 = auto-select
};
let https_addr = match https_bind {
Some(addr) => std::net::SocketAddr::from((jail_ip, addr.port())),
None => std::net::SocketAddr::from((jail_ip, 0)),
};
(Some(http_addr), Some(https_addr))
}
#[cfg(not(target_os = "linux"))]
{
None
(http_bind, https_bind)
}
};

let mut proxy = ProxyServer::new(http_port, https_port, rule_engine, bind_address);
let mut proxy = ProxyServer::new(http_bind, https_bind, rule_engine);

// Start proxy in background if running as server; otherwise start with random ports
let (actual_http_port, actual_https_port) = proxy.start().await?;

if args.run_args.server {
let bind_str = |addr: Option<std::net::SocketAddr>| {
addr.map(|a| a.ip().to_string())
.unwrap_or_else(|| "localhost".to_string())
};
info!(
"Proxy server running on http://localhost:{} and https://localhost:{}",
actual_http_port, actual_https_port
"Proxy server running on http://{}:{} and https://{}:{}",
bind_str(http_bind),
actual_http_port,
bind_str(https_bind),
actual_https_port
);
std::future::pending::<()>().await;
unreachable!();
Expand Down
92 changes: 48 additions & 44 deletions src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ 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::net::Ipv6Addr;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use tokio::net::{TcpListener, TcpStream};
Expand Down Expand Up @@ -295,35 +296,44 @@ pub fn get_client() -> &'static Client<
}

/// Try to bind to an available port in the given range (up to 16 attempts)
async fn bind_to_available_port(start: u16, end: u16, bind_addr: [u8; 4]) -> Result<TcpListener> {
async fn bind_to_available_port(start: u16, end: u16, ip: std::net::IpAddr) -> Result<TcpListener> {
let mut rng = rand::thread_rng();

for _ in 0..16 {
let port = rng.gen_range(start..=end);
match bind_ipv4_listener(bind_addr, port).await {
let addr = std::net::SocketAddr::new(ip, port);
match bind_listener(addr).await {
Ok(listener) => {
debug!("Successfully bound to port {}", port);
debug!("Successfully bound to {}:{}", ip, port);
return Ok(listener);
}
Err(_) => continue,
}
}
anyhow::bail!(
"No available port found after 16 attempts in range {}-{}",
"No available port found after 16 attempts in range {}-{} on {}",
start,
end
end,
ip
)
}

async fn bind_ipv4_listener(bind_addr: [u8; 4], port: u16) -> Result<TcpListener> {
async fn bind_listener(addr: std::net::SocketAddr) -> 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);
let is_specific_non_loopback = match addr.ip() {
std::net::IpAddr::V4(ip) => {
ip != Ipv4Addr::new(127, 0, 0, 1) && ip != Ipv4Addr::new(0, 0, 0, 0)
}
std::net::IpAddr::V6(ip) => ip != Ipv6Addr::LOCALHOST && ip != Ipv6Addr::UNSPECIFIED,
};
if is_specific_non_loopback {
let sock = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?;
let domain = match addr {
std::net::SocketAddr::V4(_) => Domain::IPV4,
std::net::SocketAddr::V6(_) => Domain::IPV6,
};
let sock = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?;
// Enabling FREEBIND for non-local address binding before interface configuration
unsafe {
let yes: libc::c_int = 1;
Expand All @@ -341,35 +351,31 @@ async fn bind_ipv4_listener(bind_addr: [u8; 4], port: u16) -> Result<TcpListener
);
}
}

sock.set_reuse_address(true)?;
sock.set_nonblocking(true)?;
let addr = SocketAddr::from((ip, port));
sock.bind(&addr.into())?;
sock.listen(1024)?; // OS default backlog
sock.listen(128)?;
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)

TcpListener::bind(addr).await.map_err(Into::into)
}

pub struct ProxyServer {
http_port: Option<u16>,
https_port: Option<u16>,
http_bind: Option<std::net::SocketAddr>,
https_bind: Option<std::net::SocketAddr>,
rule_engine: Arc<RuleEngine>,
cert_manager: Arc<CertificateManager>,
bind_address: [u8; 4],
}

impl ProxyServer {
pub fn new(
http_port: Option<u16>,
https_port: Option<u16>,
http_bind: Option<std::net::SocketAddr>,
https_bind: Option<std::net::SocketAddr>,
rule_engine: RuleEngine,
bind_address: Option<[u8; 4]>,
) -> Self {
let cert_manager = CertificateManager::new().expect("Failed to create certificate manager");

Expand All @@ -378,22 +384,20 @@ impl ProxyServer {
init_client_with_ca(ca_cert_der);

ProxyServer {
http_port,
https_port,
http_bind,
https_bind,
rule_engine: Arc::new(rule_engine),
cert_manager: Arc::new(cert_manager),
bind_address: bind_address.unwrap_or([127, 0, 0, 1]),
}
}

pub async fn start(&mut self) -> Result<(u16, u16)> {
let http_listener = if let Some(port) = self.http_port {
bind_ipv4_listener(self.bind_address, port).await?
// Bind HTTP listener
let http_listener = if let Some(addr) = self.http_bind {
bind_listener(addr).await?
} else {
// No port specified, find available port in 8000-8999 range
let listener = bind_to_available_port(8000, 8999, self.bind_address).await?;
self.http_port = Some(listener.local_addr()?.port());
listener
// No address specified, find available port in 8000-8999 range on localhost
bind_to_available_port(8000, 8999, std::net::IpAddr::V4(Ipv4Addr::LOCALHOST)).await?
};

let http_port = http_listener.local_addr()?.port();
Expand Down Expand Up @@ -429,14 +433,12 @@ impl ProxyServer {

// IPv6-specific listener not required; IPv4 listener suffices for jail routing

// Start HTTPS proxy
let https_listener = if let Some(port) = self.https_port {
bind_ipv4_listener(self.bind_address, port).await?
// Bind HTTPS listener
let https_listener = if let Some(addr) = self.https_bind {
bind_listener(addr).await?
} else {
// No port specified, find available port in 8000-8999 range
let listener = bind_to_available_port(8000, 8999, self.bind_address).await?;
self.https_port = Some(listener.local_addr()?.port());
listener
// No address specified, find available port in 8000-8999 range on localhost
bind_to_available_port(8000, 8999, std::net::IpAddr::V4(Ipv4Addr::LOCALHOST)).await?
};

let https_port = https_listener.local_addr()?.port();
Expand Down Expand Up @@ -693,17 +695,19 @@ mod tests {
let engine = V8JsRuleEngine::new(js.to_string()).unwrap();
let rule_engine = RuleEngine::from_trait(Box::new(engine), None);

let proxy = ProxyServer::new(Some(8080), Some(8443), rule_engine, None);
let http_bind = Some("127.0.0.1:8080".parse().unwrap());
let https_bind = Some("127.0.0.1:8443".parse().unwrap());
let proxy = ProxyServer::new(http_bind, https_bind, rule_engine);

assert_eq!(proxy.http_port, Some(8080));
assert_eq!(proxy.https_port, Some(8443));
assert_eq!(proxy.http_bind.map(|s| s.port()), Some(8080));
assert_eq!(proxy.https_bind.map(|s| s.port()), Some(8443));
}

#[tokio::test]
async fn test_proxy_server_auto_port() {
let engine = V8JsRuleEngine::new("true".to_string()).unwrap();
let rule_engine = RuleEngine::from_trait(Box::new(engine), None);
let mut proxy = ProxyServer::new(None, None, rule_engine, None);
let mut proxy = ProxyServer::new(None, None, rule_engine);

let (http_port, https_port) = proxy.start().await.unwrap();

Expand Down
Loading