Skip to content

Commit 81f4030

Browse files
committed
Clean up bind configuration logic
- Port-only config (e.g., '8080') now binds to 0.0.0.0 (all interfaces) following Go convention - DRY up bind_str logic with closure instead of duplicating for HTTP/HTTPS - DRY up bind resolution with resolve_bind_with_default() helper - Simplify parse_ip_from_env() to one-liner - Add #[serial] to all server bind tests to prevent port conflicts - Respect explicit port 0 for OS auto-selection - Clean verbosity logic: server mode defaults to INFO level Fixes #79
1 parent 0cbdd8e commit 81f4030

2 files changed

Lines changed: 171 additions & 16 deletions

File tree

src/main.rs

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,12 @@ async fn main() -> Result<()> {
359359
}
360360
}
361361

362-
setup_logging(args.run_args.verbose);
362+
// Server mode defaults to INFO level logging for visibility
363+
let verbosity = args
364+
.run_args
365+
.verbose
366+
.max(if args.run_args.server { 1 } else { 0 });
367+
setup_logging(verbosity);
363368

364369
// Log the version at startup for easier diagnostics
365370
debug!("httpjail version: {}", env!("VERSION_WITH_GIT_HASH"));
@@ -483,32 +488,70 @@ async fn main() -> Result<()> {
483488
}
484489

485490
// Parse bind configuration from env vars
486-
// Supports both "port" and "ip:port" formats
491+
// Returns Some(addr) for "port" or "ip:port" formats (including explicit :0)
492+
// Returns None for "ip" only or missing config
487493
fn parse_bind_config(env_var: &str) -> Option<std::net::SocketAddr> {
488494
if let Ok(val) = std::env::var(env_var) {
489-
// First try parsing as "ip:port"
495+
// First try parsing as "ip:port" (respects explicit :0)
490496
if let Ok(addr) = val.parse::<std::net::SocketAddr>() {
491497
return Some(addr);
492498
}
493-
// Try parsing as just a port number, defaulting to localhost
499+
// Try parsing as just a port number - bind to all interfaces (0.0.0.0)
494500
if let Ok(port) = val.parse::<u16>() {
495-
return Some(std::net::SocketAddr::from(([127, 0, 0, 1], port)));
501+
return Some(std::net::SocketAddr::from(([0, 0, 0, 0], port)));
496502
}
497503
}
498504
None
499505
}
500506

507+
// Parse IP-only from env var (for default port handling)
508+
fn parse_ip_from_env(env_var: &str) -> Option<std::net::IpAddr> {
509+
std::env::var(env_var).ok()?.parse().ok()
510+
}
511+
512+
// Resolve bind address with optional default port for IP-only configs
513+
fn resolve_bind_with_default(
514+
parsed: Option<std::net::SocketAddr>,
515+
env_var: &str,
516+
default_ip: std::net::IpAddr,
517+
default_port: u16,
518+
) -> Option<std::net::SocketAddr> {
519+
match parsed {
520+
Some(addr) => Some(addr), // Respect explicit config including :0
521+
None => {
522+
// Check if user provided just IP without port
523+
if let Some(ip) = parse_ip_from_env(env_var) {
524+
Some(std::net::SocketAddr::new(ip, default_port))
525+
} else {
526+
Some(std::net::SocketAddr::new(default_ip, default_port))
527+
}
528+
}
529+
}
530+
}
531+
501532
// Determine bind addresses
502533
let http_bind = parse_bind_config("HTTPJAIL_HTTP_BIND");
503534
let https_bind = parse_bind_config("HTTPJAIL_HTTPS_BIND");
504535

505536
// For strong jail mode (not weak, not server), we need to bind to a specific IP
506537
// so the proxy is accessible from the veth interface. For weak mode or server mode,
507-
// use the configured address or None (will auto-select).
538+
// use the configured address or defaults.
508539
// TODO: This has security implications - see GitHub issue #31
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)
540+
let (http_bind, https_bind) = if args.run_args.server {
541+
// Server mode: default to localhost:8080/8443, respect explicit ports including :0
542+
let localhost = std::net::IpAddr::from([127, 0, 0, 1]);
543+
let http = resolve_bind_with_default(http_bind, "HTTPJAIL_HTTP_BIND", localhost, 8080);
544+
let https = resolve_bind_with_default(https_bind, "HTTPJAIL_HTTPS_BIND", localhost, 8443);
545+
(http, https)
546+
} else if args.run_args.weak {
547+
// Weak mode: If IP-only provided, use port 0 (OS auto-select), else None
548+
let http = http_bind.or_else(|| {
549+
parse_ip_from_env("HTTPJAIL_HTTP_BIND").map(|ip| std::net::SocketAddr::new(ip, 0))
550+
});
551+
let https = https_bind.or_else(|| {
552+
parse_ip_from_env("HTTPJAIL_HTTPS_BIND").map(|ip| std::net::SocketAddr::new(ip, 0))
553+
});
554+
(http, https)
512555
} else {
513556
#[cfg(target_os = "linux")]
514557
{
@@ -538,15 +581,16 @@ async fn main() -> Result<()> {
538581
let (actual_http_port, actual_https_port) = proxy.start().await?;
539582

540583
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());
584+
let bind_str = |addr: Option<std::net::SocketAddr>| {
585+
addr.map(|a| a.ip().to_string())
586+
.unwrap_or_else(|| "localhost".to_string())
587+
};
547588
info!(
548589
"Proxy server running on http://{}:{} and https://{}:{}",
549-
http_bind_str, actual_http_port, https_bind_str, actual_https_port
590+
bind_str(http_bind),
591+
actual_http_port,
592+
bind_str(https_bind),
593+
actual_https_port
550594
);
551595
std::future::pending::<()>().await;
552596
unreachable!();

tests/weak_integration.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod common;
22

33
use common::{HttpjailCommand, test_https_allow, test_https_blocking};
4+
use serial_test::serial;
45
use std::process::{Command, Stdio};
56
use std::str::FromStr;
67
use std::thread;
@@ -288,6 +289,116 @@ fn test_server_mode() {
288289
let _ = server.wait();
289290
}
290291

292+
// Helper to start server with custom bind config
293+
fn start_server_with_bind(http_bind: &str, https_bind: &str) -> (std::process::Child, u16) {
294+
let httpjail_path: &str = env!("CARGO_BIN_EXE_httpjail");
295+
296+
let mut child = Command::new(httpjail_path)
297+
.arg("--server")
298+
.arg("--js")
299+
.arg("true")
300+
.env("HTTPJAIL_HTTP_BIND", http_bind)
301+
.env("HTTPJAIL_HTTPS_BIND", https_bind)
302+
.env("HTTPJAIL_SKIP_KEYCHAIN_INSTALL", "1")
303+
.stdout(Stdio::null())
304+
.stderr(Stdio::null())
305+
.spawn()
306+
.expect("Failed to spawn server");
307+
308+
// Parse expected port from bind config (server mode uses defaults for unspecified)
309+
let expected_port = if let Ok(port) = http_bind.parse::<u16>() {
310+
port
311+
} else if let Ok(addr) = http_bind.parse::<std::net::SocketAddr>() {
312+
addr.port()
313+
} else {
314+
8080 // Default port for server mode
315+
};
316+
317+
// Wait for server to bind
318+
if !wait_for_server(expected_port, Duration::from_secs(3)) {
319+
child.kill().ok();
320+
panic!("Server failed to bind to port {}", expected_port);
321+
}
322+
323+
(child, expected_port)
324+
}
325+
326+
#[test]
327+
#[serial]
328+
fn test_server_bind_defaults() {
329+
let (mut server, port) = start_server_with_bind("", "");
330+
assert_eq!(port, 8080, "Server should default to port 8080");
331+
server.kill().ok();
332+
}
333+
334+
#[test]
335+
#[serial]
336+
fn test_server_bind_port_only() {
337+
// Port-only should bind to all interfaces (0.0.0.0)
338+
let (mut server, port) = start_server_with_bind("19882", "19883");
339+
assert_eq!(port, 19882, "Server should bind to specified port on all interfaces");
340+
server.kill().ok();
341+
}
342+
343+
#[test]
344+
#[serial]
345+
fn test_server_bind_all_interfaces() {
346+
let (mut server, port) = start_server_with_bind("0.0.0.0:19884", "0.0.0.0:19885");
347+
assert_eq!(
348+
port, 19884,
349+
"Server should bind to specified port on 0.0.0.0"
350+
);
351+
server.kill().ok();
352+
}
353+
354+
#[test]
355+
#[serial]
356+
fn test_server_bind_ip_without_port() {
357+
let (mut server, port) = start_server_with_bind("127.0.0.1", "127.0.0.1");
358+
assert_eq!(
359+
port, 8080,
360+
"Server should use default port 8080 when only IP specified"
361+
);
362+
server.kill().ok();
363+
}
364+
365+
#[test]
366+
#[serial]
367+
fn test_server_bind_explicit_port_zero() {
368+
// Explicit port 0 should be respected (OS auto-select), not overridden to 8080
369+
let httpjail_path: &str = env!("CARGO_BIN_EXE_httpjail");
370+
371+
let mut child = Command::new(httpjail_path)
372+
.arg("--server")
373+
.arg("--js")
374+
.arg("true")
375+
.env("HTTPJAIL_HTTP_BIND", "0") // Explicit port 0
376+
.env("HTTPJAIL_HTTPS_BIND", "0")
377+
.env("HTTPJAIL_SKIP_KEYCHAIN_INSTALL", "1")
378+
.stdout(Stdio::null())
379+
.stderr(Stdio::null())
380+
.spawn()
381+
.expect("Failed to spawn server");
382+
383+
// Give server a moment to bind to an OS-selected port
384+
thread::sleep(Duration::from_millis(500));
385+
386+
// Verify it's NOT bound to the default port 8080 (it should be random)
387+
let not_on_default = std::net::TcpStream::connect("127.0.0.1:8080").is_err();
388+
assert!(
389+
not_on_default,
390+
"Server should not bind to default port 8080 when explicit :0 provided"
391+
);
392+
393+
// Server should still be running successfully
394+
assert!(
395+
child.try_wait().unwrap().is_none(),
396+
"Server should be running"
397+
);
398+
399+
child.kill().ok();
400+
}
401+
291402
/// Test for Host header security (Issue #57)
292403
/// Verifies that httpjail corrects mismatched Host headers to prevent
293404
/// CloudFlare and other CDN routing bypasses.

0 commit comments

Comments
 (0)