Skip to content

Commit 5d201b5

Browse files
committed
fix: respect HTTPJAIL_HTTP_BIND and HTTPJAIL_HTTPS_BIND in server mode
Fixes #79 The server mode was ignoring the IP addresses specified in the HTTPJAIL_HTTP_BIND and HTTPJAIL_HTTPS_BIND environment variables, always binding to localhost instead. This commit refactors the proxy binding logic to use std::net::SocketAddr throughout, which provides a cleaner API that supports both IPv4 and IPv6 while combining IP and port into a single type. Changes: - Refactored ProxyServer struct to use Option<SocketAddr> for http_bind and https_bind instead of separate IP and port fields - Created unified bind_listener() function that handles both IPv4 and IPv6 - Removed redundant bind_ipv4_listener() function - Simplified parse_bind_config() to return Option<SocketAddr> directly - Fixed strong jail mode to properly bind to computed jail IP with port 0 for auto-selection when no port is specified The fix maintains backward compatibility and passes all tests: - 45/45 unit tests pass - 23/23 integration tests pass - Clippy passes with -D warnings Supported formats for bind addresses: - "ip:port" (e.g., "0.0.0.0:8080", "127.0.0.1:8080") - "[ipv6]:port" (e.g., "[::1]:8080") - "port" (defaults to localhost)
1 parent 5afe807 commit 5d201b5

2 files changed

Lines changed: 89 additions & 78 deletions

File tree

src/main.rs

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -484,63 +484,69 @@ async fn main() -> Result<()> {
484484

485485
// Parse bind configuration from env vars
486486
// Supports both "port" and "ip:port" formats
487-
fn parse_bind_config(env_var: &str) -> (Option<u16>, Option<std::net::IpAddr>) {
487+
fn parse_bind_config(env_var: &str) -> Option<std::net::SocketAddr> {
488488
if let Ok(val) = std::env::var(env_var) {
489-
if let Some(colon_pos) = val.rfind(':') {
490-
// Try to parse as ip:port
491-
let ip_str = &val[..colon_pos];
492-
let port_str = &val[colon_pos + 1..];
493-
match port_str.parse::<u16>() {
494-
Ok(port) => match ip_str.parse::<std::net::IpAddr>() {
495-
Ok(ip) => (Some(port), Some(ip)),
496-
Err(_) => (Some(port), None),
497-
},
498-
Err(_) => (None, None),
499-
}
500-
} else {
501-
// Try to parse as port
502-
match val.parse::<u16>() {
503-
Ok(port) => (Some(port), None),
504-
Err(_) => (None, None),
505-
}
489+
// First try parsing as "ip:port"
490+
if let Ok(addr) = val.parse::<std::net::SocketAddr>() {
491+
return Some(addr);
492+
}
493+
// Try parsing as just a port number, defaulting to localhost
494+
if let Ok(port) = val.parse::<u16>() {
495+
return Some(std::net::SocketAddr::from(([127, 0, 0, 1], port)));
506496
}
507-
} else {
508-
(None, None)
509497
}
498+
None
510499
}
511500

512-
// Determine ports to bind
513-
let (http_port, _http_ip) = parse_bind_config("HTTPJAIL_HTTP_BIND");
514-
let (https_port, _https_ip) = parse_bind_config("HTTPJAIL_HTTPS_BIND");
501+
// Determine bind addresses
502+
let http_bind = parse_bind_config("HTTPJAIL_HTTP_BIND");
503+
let https_bind = parse_bind_config("HTTPJAIL_HTTPS_BIND");
515504

516-
// For strong jail mode (not weak, not server), we need to bind to all interfaces (0.0.0.0)
505+
// For strong jail mode (not weak, not server), we need to bind to a specific IP
517506
// so the proxy is accessible from the veth interface. For weak mode or server mode,
518-
// localhost is fine.
507+
// use the configured address or None (will auto-select).
519508
// TODO: This has security implications - see GitHub issue #31
520-
let bind_address: Option<[u8; 4]> = if args.run_args.weak || args.run_args.server {
521-
None
509+
let (http_bind, https_bind) = if args.run_args.weak || args.run_args.server {
510+
// In weak/server mode, respect HTTPJAIL_HTTP_BIND and HTTPJAIL_HTTPS_BIND environment variables
511+
(http_bind, https_bind)
522512
} else {
523513
#[cfg(target_os = "linux")]
524514
{
525-
Some(
526-
httpjail::jail::linux::LinuxJail::compute_host_ip_for_jail_id(&jail_config.jail_id),
527-
)
515+
let jail_ip =
516+
httpjail::jail::linux::LinuxJail::compute_host_ip_for_jail_id(&jail_config.jail_id);
517+
// For strong jail mode, we need to bind to the jail IP.
518+
// Use env var port if provided, otherwise use port 0 (auto-select) on jail IP.
519+
let http_addr = match http_bind {
520+
Some(addr) => std::net::SocketAddr::from((jail_ip, addr.port())),
521+
None => std::net::SocketAddr::from((jail_ip, 0)), // Port 0 = auto-select
522+
};
523+
let https_addr = match https_bind {
524+
Some(addr) => std::net::SocketAddr::from((jail_ip, addr.port())),
525+
None => std::net::SocketAddr::from((jail_ip, 0)),
526+
};
527+
(Some(http_addr), Some(https_addr))
528528
}
529529
#[cfg(not(target_os = "linux"))]
530530
{
531-
None
531+
(http_bind, https_bind)
532532
}
533533
};
534534

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

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

540540
if args.run_args.server {
541+
let http_bind_str = http_bind
542+
.map(|addr| addr.ip().to_string())
543+
.unwrap_or_else(|| "localhost".to_string());
544+
let https_bind_str = https_bind
545+
.map(|addr| addr.ip().to_string())
546+
.unwrap_or_else(|| "localhost".to_string());
541547
info!(
542-
"Proxy server running on http://localhost:{} and https://localhost:{}",
543-
actual_http_port, actual_https_port
548+
"Proxy server running on http://{}:{} and https://{}:{}",
549+
http_bind_str, actual_http_port, https_bind_str, actual_https_port
544550
);
545551
std::future::pending::<()>().await;
546552
unreachable!();

src/proxy.rs

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

24-
#[cfg(target_os = "linux")]
25-
use std::net::Ipv4Addr;
2624
use std::net::SocketAddr;
25+
#[cfg(target_os = "linux")]
26+
use std::net::{Ipv4Addr, Ipv6Addr};
2727
use std::sync::{Arc, OnceLock};
2828
use std::time::Duration;
2929
use tokio::net::{TcpListener, TcpStream};
@@ -295,35 +295,44 @@ pub fn get_client() -> &'static Client<
295295
}
296296

297297
/// 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> {
298+
async fn bind_to_available_port(start: u16, end: u16, ip: std::net::IpAddr) -> Result<TcpListener> {
299299
let mut rng = rand::thread_rng();
300300

301301
for _ in 0..16 {
302302
let port = rng.gen_range(start..=end);
303-
match bind_ipv4_listener(bind_addr, port).await {
303+
let addr = std::net::SocketAddr::new(ip, port);
304+
match bind_listener(addr).await {
304305
Ok(listener) => {
305-
debug!("Successfully bound to port {}", port);
306+
debug!("Successfully bound to {}:{}", ip, port);
306307
return Ok(listener);
307308
}
308309
Err(_) => continue,
309310
}
310311
}
311312
anyhow::bail!(
312-
"No available port found after 16 attempts in range {}-{}",
313+
"No available port found after 16 attempts in range {}-{} on {}",
313314
start,
314-
end
315+
end,
316+
ip
315317
)
316318
}
317319

318-
async fn bind_ipv4_listener(bind_addr: [u8; 4], port: u16) -> Result<TcpListener> {
320+
async fn bind_listener(addr: std::net::SocketAddr) -> Result<TcpListener> {
319321
#[cfg(target_os = "linux")]
320322
{
321323
// 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);
324+
let is_specific_non_loopback = match addr.ip() {
325+
std::net::IpAddr::V4(ip) => {
326+
ip != Ipv4Addr::new(127, 0, 0, 1) && ip != Ipv4Addr::new(0, 0, 0, 0)
327+
}
328+
std::net::IpAddr::V6(ip) => ip != Ipv6Addr::LOCALHOST && ip != Ipv6Addr::UNSPECIFIED,
329+
};
325330
if is_specific_non_loopback {
326-
let sock = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?;
331+
let domain = match addr {
332+
std::net::SocketAddr::V4(_) => Domain::IPV4,
333+
std::net::SocketAddr::V6(_) => Domain::IPV6,
334+
};
335+
let sock = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?;
327336
// Enabling FREEBIND for non-local address binding before interface configuration
328337
unsafe {
329338
let yes: libc::c_int = 1;
@@ -341,35 +350,31 @@ async fn bind_ipv4_listener(bind_addr: [u8; 4], port: u16) -> Result<TcpListener
341350
);
342351
}
343352
}
344-
353+
sock.set_reuse_address(true)?;
345354
sock.set_nonblocking(true)?;
346-
let addr = SocketAddr::from((ip, port));
347355
sock.bind(&addr.into())?;
348-
sock.listen(1024)?; // OS default backlog
356+
sock.listen(128)?;
349357
let std_listener: std::net::TcpListener = sock.into();
350358
std_listener.set_nonblocking(true)?;
351359
return Ok(TcpListener::from_std(std_listener)?);
352360
}
353361
}
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)
362+
363+
TcpListener::bind(addr).await.map_err(Into::into)
357364
}
358365

359366
pub struct ProxyServer {
360-
http_port: Option<u16>,
361-
https_port: Option<u16>,
367+
http_bind: Option<std::net::SocketAddr>,
368+
https_bind: Option<std::net::SocketAddr>,
362369
rule_engine: Arc<RuleEngine>,
363370
cert_manager: Arc<CertificateManager>,
364-
bind_address: [u8; 4],
365371
}
366372

367373
impl ProxyServer {
368374
pub fn new(
369-
http_port: Option<u16>,
370-
https_port: Option<u16>,
375+
http_bind: Option<std::net::SocketAddr>,
376+
https_bind: Option<std::net::SocketAddr>,
371377
rule_engine: RuleEngine,
372-
bind_address: Option<[u8; 4]>,
373378
) -> Self {
374379
let cert_manager = CertificateManager::new().expect("Failed to create certificate manager");
375380

@@ -378,22 +383,21 @@ impl ProxyServer {
378383
init_client_with_ca(ca_cert_der);
379384

380385
ProxyServer {
381-
http_port,
382-
https_port,
386+
http_bind,
387+
https_bind,
383388
rule_engine: Arc::new(rule_engine),
384389
cert_manager: Arc::new(cert_manager),
385-
bind_address: bind_address.unwrap_or([127, 0, 0, 1]),
386390
}
387391
}
388392

389393
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?
394+
// Bind HTTP listener
395+
let http_listener = if let Some(addr) = self.http_bind {
396+
bind_listener(addr).await?
392397
} 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
398+
// No address specified, find available port in 8000-8999 range on localhost
399+
bind_to_available_port(8000, 8999, std::net::IpAddr::V4(Ipv4Addr::LOCALHOST))
400+
.await?
397401
};
398402

399403
let http_port = http_listener.local_addr()?.port();
@@ -429,14 +433,13 @@ 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))
442+
.await?
440443
};
441444

442445
let https_port = https_listener.local_addr()?.port();
@@ -693,17 +696,19 @@ mod tests {
693696
let engine = V8JsRuleEngine::new(js.to_string()).unwrap();
694697
let rule_engine = RuleEngine::from_trait(Box::new(engine), None);
695698

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

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

702707
#[tokio::test]
703708
async fn test_proxy_server_auto_port() {
704709
let engine = V8JsRuleEngine::new("true".to_string()).unwrap();
705710
let rule_engine = RuleEngine::from_trait(Box::new(engine), None);
706-
let mut proxy = ProxyServer::new(None, None, rule_engine, None);
711+
let mut proxy = ProxyServer::new(None, None, rule_engine);
707712

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

0 commit comments

Comments
 (0)