Skip to content

Commit 321d0e6

Browse files
authored
fix: respect HTTPJAIL_HTTP_BIND and HTTPJAIL_HTTPS_BIND in server mode (#80)
1 parent 078ccea commit 321d0e6

File tree

3 files changed

+274
-81
lines changed

3 files changed

+274
-81
lines changed

src/main.rs

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,12 @@ async fn main() -> Result<()> {
363363
}
364364
}
365365

366-
setup_logging(args.run_args.verbose);
366+
// Server mode defaults to INFO level logging for visibility
367+
let verbosity = args
368+
.run_args
369+
.verbose
370+
.max(if args.run_args.server { 1 } else { 0 });
371+
setup_logging(verbosity);
367372

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

489494
// Parse bind configuration from env vars
490-
// Supports both "port" and "ip:port" formats
491-
fn parse_bind_config(env_var: &str) -> (Option<u16>, Option<std::net::IpAddr>) {
495+
// Returns Some(addr) for "port", ":port", or "ip:port" formats (including explicit :0)
496+
// Returns None for "ip" only or missing config
497+
fn parse_bind_config(env_var: &str) -> Option<std::net::SocketAddr> {
492498
if let Ok(val) = std::env::var(env_var) {
493-
if let Some(colon_pos) = val.rfind(':') {
494-
// Try to parse as ip:port
495-
let ip_str = &val[..colon_pos];
496-
let port_str = &val[colon_pos + 1..];
497-
match port_str.parse::<u16>() {
498-
Ok(port) => match ip_str.parse::<std::net::IpAddr>() {
499-
Ok(ip) => (Some(port), Some(ip)),
500-
Err(_) => (Some(port), None),
501-
},
502-
Err(_) => (None, None),
503-
}
504-
} else {
505-
// Try to parse as port
506-
match val.parse::<u16>() {
507-
Ok(port) => (Some(port), None),
508-
Err(_) => (None, None),
499+
let val = val.trim();
500+
501+
// First try parsing as "ip:port" (respects explicit :0)
502+
if let Ok(addr) = val.parse::<std::net::SocketAddr>() {
503+
return Some(addr);
504+
}
505+
506+
// Try parsing as ":port" (Go-style) - bind to all interfaces (0.0.0.0)
507+
if let Some(port_str) = val.strip_prefix(':')
508+
&& let Ok(port) = port_str.parse::<u16>()
509+
{
510+
return Some(std::net::SocketAddr::from(([0, 0, 0, 0], port)));
511+
}
512+
513+
// Try parsing as just a port number - bind to all interfaces (0.0.0.0)
514+
if let Ok(port) = val.parse::<u16>() {
515+
return Some(std::net::SocketAddr::from(([0, 0, 0, 0], port)));
516+
}
517+
}
518+
None
519+
}
520+
521+
// Parse IP-only from env var (for default port handling)
522+
fn parse_ip_from_env(env_var: &str) -> Option<std::net::IpAddr> {
523+
std::env::var(env_var).ok()?.parse().ok()
524+
}
525+
526+
// Resolve bind address with optional default port for IP-only configs
527+
fn resolve_bind_with_default(
528+
parsed: Option<std::net::SocketAddr>,
529+
env_var: &str,
530+
default_ip: std::net::IpAddr,
531+
default_port: u16,
532+
) -> Option<std::net::SocketAddr> {
533+
match parsed {
534+
Some(addr) => Some(addr), // Respect explicit config including :0
535+
None => {
536+
// Check if user provided just IP without port
537+
if let Some(ip) = parse_ip_from_env(env_var) {
538+
Some(std::net::SocketAddr::new(ip, default_port))
539+
} else {
540+
Some(std::net::SocketAddr::new(default_ip, default_port))
509541
}
510542
}
511-
} else {
512-
(None, None)
513543
}
514544
}
515545

516-
// Determine ports to bind
517-
let (http_port, _http_ip) = parse_bind_config("HTTPJAIL_HTTP_BIND");
518-
let (https_port, _https_ip) = parse_bind_config("HTTPJAIL_HTTPS_BIND");
546+
// Determine bind addresses
547+
let http_bind = parse_bind_config("HTTPJAIL_HTTP_BIND");
548+
let https_bind = parse_bind_config("HTTPJAIL_HTTPS_BIND");
519549

520-
// For strong jail mode (not weak, not server), we need to bind to all interfaces (0.0.0.0)
550+
// For strong jail mode (not weak, not server), we need to bind to a specific IP
521551
// so the proxy is accessible from the veth interface. For weak mode or server mode,
522-
// localhost is fine.
552+
// use the configured address or defaults.
523553
// TODO: This has security implications - see GitHub issue #31
524-
let bind_address: Option<[u8; 4]> = if args.run_args.weak || args.run_args.server {
525-
None
554+
let (http_bind, https_bind) = if args.run_args.server {
555+
// Server mode: default to localhost:8080/8443, respect explicit ports including :0
556+
let localhost = std::net::IpAddr::from([127, 0, 0, 1]);
557+
let http = resolve_bind_with_default(http_bind, "HTTPJAIL_HTTP_BIND", localhost, 8080);
558+
let https = resolve_bind_with_default(https_bind, "HTTPJAIL_HTTPS_BIND", localhost, 8443);
559+
(http, https)
560+
} else if args.run_args.weak {
561+
// Weak mode: If IP-only provided, use port 0 (OS auto-select), else None
562+
let http = http_bind.or_else(|| {
563+
parse_ip_from_env("HTTPJAIL_HTTP_BIND").map(|ip| std::net::SocketAddr::new(ip, 0))
564+
});
565+
let https = https_bind.or_else(|| {
566+
parse_ip_from_env("HTTPJAIL_HTTPS_BIND").map(|ip| std::net::SocketAddr::new(ip, 0))
567+
});
568+
(http, https)
526569
} else {
527570
#[cfg(target_os = "linux")]
528571
{
529-
Some(
530-
httpjail::jail::linux::LinuxJail::compute_host_ip_for_jail_id(&jail_config.jail_id),
531-
)
572+
let jail_ip =
573+
httpjail::jail::linux::LinuxJail::compute_host_ip_for_jail_id(&jail_config.jail_id);
574+
// For strong jail mode, we need to bind to the jail IP.
575+
// Use env var port if provided, otherwise use port 0 (auto-select) on jail IP.
576+
let http_addr = match http_bind {
577+
Some(addr) => std::net::SocketAddr::from((jail_ip, addr.port())),
578+
None => std::net::SocketAddr::from((jail_ip, 0)), // Port 0 = auto-select
579+
};
580+
let https_addr = match https_bind {
581+
Some(addr) => std::net::SocketAddr::from((jail_ip, addr.port())),
582+
None => std::net::SocketAddr::from((jail_ip, 0)),
583+
};
584+
(Some(http_addr), Some(https_addr))
532585
}
533586
#[cfg(not(target_os = "linux"))]
534587
{
535-
None
588+
(http_bind, https_bind)
536589
}
537590
};
538591

539-
let mut proxy = ProxyServer::new(http_port, https_port, rule_engine, bind_address);
592+
let mut proxy = ProxyServer::new(http_bind, https_bind, rule_engine);
540593

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

544597
if args.run_args.server {
598+
let bind_str = |addr: Option<std::net::SocketAddr>| {
599+
addr.map(|a| a.ip().to_string())
600+
.unwrap_or_else(|| "localhost".to_string())
601+
};
545602
info!(
546-
"Proxy server running on http://localhost:{} and https://localhost:{}",
547-
actual_http_port, actual_https_port
603+
"Proxy server running on http://{}:{} and https://{}:{}",
604+
bind_str(http_bind),
605+
actual_http_port,
606+
bind_str(https_bind),
607+
actual_https_port
548608
);
549609
std::future::pending::<()>().await;
550610
unreachable!();

src/proxy.rs

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ use std::os::fd::AsRawFd;
2121
#[cfg(target_os = "linux")]
2222
use socket2::{Domain, Protocol, Socket, Type};
2323

24+
use std::net::{Ipv4Addr, SocketAddr};
25+
2426
#[cfg(target_os = "linux")]
25-
use std::net::Ipv4Addr;
26-
use std::net::SocketAddr;
27+
use std::net::Ipv6Addr;
2728
use std::sync::{Arc, OnceLock};
2829
use std::time::Duration;
2930
use tokio::net::{TcpListener, TcpStream};
@@ -295,35 +296,44 @@ pub fn get_client() -> &'static Client<
295296
}
296297

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

301302
for _ in 0..16 {
302303
let port = rng.gen_range(start..=end);
303-
match bind_ipv4_listener(bind_addr, port).await {
304+
let addr = std::net::SocketAddr::new(ip, port);
305+
match bind_listener(addr).await {
304306
Ok(listener) => {
305-
debug!("Successfully bound to port {}", port);
307+
debug!("Successfully bound to {}:{}", ip, port);
306308
return Ok(listener);
307309
}
308310
Err(_) => continue,
309311
}
310312
}
311313
anyhow::bail!(
312-
"No available port found after 16 attempts in range {}-{}",
314+
"No available port found after 16 attempts in range {}-{} on {}",
313315
start,
314-
end
316+
end,
317+
ip
315318
)
316319
}
317320

318-
async fn bind_ipv4_listener(bind_addr: [u8; 4], port: u16) -> Result<TcpListener> {
321+
async fn bind_listener(addr: std::net::SocketAddr) -> Result<TcpListener> {
319322
#[cfg(target_os = "linux")]
320323
{
321324
// Setup a raw socket to set IP_FREEBIND for specific non-loopback addresses
322-
let ip = Ipv4Addr::from(bind_addr);
323-
let is_specific_non_loopback =
324-
ip != Ipv4Addr::new(127, 0, 0, 1) && ip != Ipv4Addr::new(0, 0, 0, 0);
325+
let is_specific_non_loopback = match addr.ip() {
326+
std::net::IpAddr::V4(ip) => {
327+
ip != Ipv4Addr::new(127, 0, 0, 1) && ip != Ipv4Addr::new(0, 0, 0, 0)
328+
}
329+
std::net::IpAddr::V6(ip) => ip != Ipv6Addr::LOCALHOST && ip != Ipv6Addr::UNSPECIFIED,
330+
};
325331
if is_specific_non_loopback {
326-
let sock = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?;
332+
let domain = match addr {
333+
std::net::SocketAddr::V4(_) => Domain::IPV4,
334+
std::net::SocketAddr::V6(_) => Domain::IPV6,
335+
};
336+
let sock = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?;
327337
// Enabling FREEBIND for non-local address binding before interface configuration
328338
unsafe {
329339
let yes: libc::c_int = 1;
@@ -341,35 +351,31 @@ async fn bind_ipv4_listener(bind_addr: [u8; 4], port: u16) -> Result<TcpListener
341351
);
342352
}
343353
}
344-
354+
sock.set_reuse_address(true)?;
345355
sock.set_nonblocking(true)?;
346-
let addr = SocketAddr::from((ip, port));
347356
sock.bind(&addr.into())?;
348-
sock.listen(1024)?; // OS default backlog
357+
sock.listen(128)?;
349358
let std_listener: std::net::TcpListener = sock.into();
350359
std_listener.set_nonblocking(true)?;
351360
return Ok(TcpListener::from_std(std_listener)?);
352361
}
353362
}
354-
// Fallback: normal async bind if the conditions aren't met
355-
let listener = TcpListener::bind(SocketAddr::from((bind_addr, port))).await?;
356-
Ok(listener)
363+
364+
TcpListener::bind(addr).await.map_err(Into::into)
357365
}
358366

359367
pub struct ProxyServer {
360-
http_port: Option<u16>,
361-
https_port: Option<u16>,
368+
http_bind: Option<std::net::SocketAddr>,
369+
https_bind: Option<std::net::SocketAddr>,
362370
rule_engine: Arc<RuleEngine>,
363371
cert_manager: Arc<CertificateManager>,
364-
bind_address: [u8; 4],
365372
}
366373

367374
impl ProxyServer {
368375
pub fn new(
369-
http_port: Option<u16>,
370-
https_port: Option<u16>,
376+
http_bind: Option<std::net::SocketAddr>,
377+
https_bind: Option<std::net::SocketAddr>,
371378
rule_engine: RuleEngine,
372-
bind_address: Option<[u8; 4]>,
373379
) -> Self {
374380
let cert_manager = CertificateManager::new().expect("Failed to create certificate manager");
375381

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

380386
ProxyServer {
381-
http_port,
382-
https_port,
387+
http_bind,
388+
https_bind,
383389
rule_engine: Arc::new(rule_engine),
384390
cert_manager: Arc::new(cert_manager),
385-
bind_address: bind_address.unwrap_or([127, 0, 0, 1]),
386391
}
387392
}
388393

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

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

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

432-
// Start HTTPS proxy
433-
let https_listener = if let Some(port) = self.https_port {
434-
bind_ipv4_listener(self.bind_address, port).await?
436+
// Bind HTTPS listener
437+
let https_listener = if let Some(addr) = self.https_bind {
438+
bind_listener(addr).await?
435439
} else {
436-
// No port specified, find available port in 8000-8999 range
437-
let listener = bind_to_available_port(8000, 8999, self.bind_address).await?;
438-
self.https_port = Some(listener.local_addr()?.port());
439-
listener
440+
// No address specified, find available port in 8000-8999 range on localhost
441+
bind_to_available_port(8000, 8999, std::net::IpAddr::V4(Ipv4Addr::LOCALHOST)).await?
440442
};
441443

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

696-
let proxy = ProxyServer::new(Some(8080), Some(8443), rule_engine, None);
698+
let http_bind = Some("127.0.0.1:8080".parse().unwrap());
699+
let https_bind = Some("127.0.0.1:8443".parse().unwrap());
700+
let proxy = ProxyServer::new(http_bind, https_bind, rule_engine);
697701

698-
assert_eq!(proxy.http_port, Some(8080));
699-
assert_eq!(proxy.https_port, Some(8443));
702+
assert_eq!(proxy.http_bind.map(|s| s.port()), Some(8080));
703+
assert_eq!(proxy.https_bind.map(|s| s.port()), Some(8443));
700704
}
701705

702706
#[tokio::test]
703707
async fn test_proxy_server_auto_port() {
704708
let engine = V8JsRuleEngine::new("true".to_string()).unwrap();
705709
let rule_engine = RuleEngine::from_trait(Box::new(engine), None);
706-
let mut proxy = ProxyServer::new(None, None, rule_engine, None);
710+
let mut proxy = ProxyServer::new(None, None, rule_engine);
707711

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

0 commit comments

Comments
 (0)