Skip to content

Commit 58fffee

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 07d0462 commit 58fffee

File tree

2 files changed

+171
-16
lines changed

2 files changed

+171
-16
lines changed

src/main.rs

Lines changed: 60 additions & 16 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,32 +492,70 @@ async fn main() -> Result<()> {
487492
}
488493

489494
// Parse bind configuration from env vars
490-
// Supports both "port" and "ip:port" formats
495+
// Returns Some(addr) for "port" or "ip:port" formats (including explicit :0)
496+
// Returns None for "ip" only or missing config
491497
fn parse_bind_config(env_var: &str) -> Option<std::net::SocketAddr> {
492498
if let Ok(val) = std::env::var(env_var) {
493-
// First try parsing as "ip:port"
499+
// First try parsing as "ip:port" (respects explicit :0)
494500
if let Ok(addr) = val.parse::<std::net::SocketAddr>() {
495501
return Some(addr);
496502
}
497-
// Try parsing as just a port number, defaulting to localhost
503+
// Try parsing as just a port number - bind to all interfaces (0.0.0.0)
498504
if let Ok(port) = val.parse::<u16>() {
499-
return Some(std::net::SocketAddr::from(([127, 0, 0, 1], port)));
505+
return Some(std::net::SocketAddr::from(([0, 0, 0, 0], port)));
500506
}
501507
}
502508
None
503509
}
504510

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

509540
// For strong jail mode (not weak, not server), we need to bind to a specific IP
510541
// so the proxy is accessible from the veth interface. For weak mode or server mode,
511-
// use the configured address or None (will auto-select).
542+
// use the configured address or defaults.
512543
// TODO: This has security implications - see GitHub issue #31
513-
let (http_bind, https_bind) = if args.run_args.weak || args.run_args.server {
514-
// In weak/server mode, respect HTTPJAIL_HTTP_BIND and HTTPJAIL_HTTPS_BIND environment variables
515-
(http_bind, https_bind)
544+
let (http_bind, https_bind) = if args.run_args.server {
545+
// Server mode: default to localhost:8080/8443, respect explicit ports including :0
546+
let localhost = std::net::IpAddr::from([127, 0, 0, 1]);
547+
let http = resolve_bind_with_default(http_bind, "HTTPJAIL_HTTP_BIND", localhost, 8080);
548+
let https = resolve_bind_with_default(https_bind, "HTTPJAIL_HTTPS_BIND", localhost, 8443);
549+
(http, https)
550+
} else if args.run_args.weak {
551+
// Weak mode: If IP-only provided, use port 0 (OS auto-select), else None
552+
let http = http_bind.or_else(|| {
553+
parse_ip_from_env("HTTPJAIL_HTTP_BIND").map(|ip| std::net::SocketAddr::new(ip, 0))
554+
});
555+
let https = https_bind.or_else(|| {
556+
parse_ip_from_env("HTTPJAIL_HTTPS_BIND").map(|ip| std::net::SocketAddr::new(ip, 0))
557+
});
558+
(http, https)
516559
} else {
517560
#[cfg(target_os = "linux")]
518561
{
@@ -542,15 +585,16 @@ async fn main() -> Result<()> {
542585
let (actual_http_port, actual_https_port) = proxy.start().await?;
543586

544587
if args.run_args.server {
545-
let http_bind_str = http_bind
546-
.map(|addr| addr.ip().to_string())
547-
.unwrap_or_else(|| "localhost".to_string());
548-
let https_bind_str = https_bind
549-
.map(|addr| addr.ip().to_string())
550-
.unwrap_or_else(|| "localhost".to_string());
588+
let bind_str = |addr: Option<std::net::SocketAddr>| {
589+
addr.map(|a| a.ip().to_string())
590+
.unwrap_or_else(|| "localhost".to_string())
591+
};
551592
info!(
552593
"Proxy server running on http://{}:{} and https://{}:{}",
553-
http_bind_str, actual_http_port, https_bind_str, actual_https_port
594+
bind_str(http_bind),
595+
actual_http_port,
596+
bind_str(https_bind),
597+
actual_https_port
554598
);
555599
std::future::pending::<()>().await;
556600
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;
@@ -289,6 +290,116 @@ fn test_server_mode() {
289290
let _ = server.wait();
290291
}
291292

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

0 commit comments

Comments
 (0)