diff --git a/src/main.rs b/src/main.rs index 940b462..ca67821 100644 --- a/src/main.rs +++ b/src/main.rs @@ -322,9 +322,30 @@ fn expand_path(path: &str) -> PathBuf { PathBuf::from(path) } -/// Check if a port is available for binding +/// Check if a port is available for binding. +/// +/// pg0 connects to the instance over `127.0.0.1`, so the **IPv4 loopback bind +/// must succeed** for the port to be usable — any IPv4 error (`AddrInUse`, or +/// `PermissionDenied` for low ports such as 80, …) means unavailable. +/// +/// PostgreSQL with the default `listen_addresses = 'localhost'` also binds +/// `::1`, so a port already held by an IPv6 or dual-stack listener — e.g. a +/// Docker-published `[::]:5432` — must be rejected too; an IPv4-only probe +/// misses it and silently collides. Only `AddrInUse` from the IPv6 probe is +/// treated as taken; other IPv6 errors (e.g. IPv6 disabled on the host) are +/// ignored so they don't make every port look unavailable. fn is_port_available(port: u16) -> bool { - std::net::TcpListener::bind(("127.0.0.1", port)).is_ok() + use std::net::{Ipv4Addr, Ipv6Addr, TcpListener}; + // pg0 connects over IPv4 loopback — require it to be bindable. + if TcpListener::bind((Ipv4Addr::LOCALHOST, port)).is_err() { + return false; + } + // Reject ports held by an IPv6/dual-stack listener; ignore non-AddrInUse + // IPv6 errors (e.g. IPv6 disabled). + !matches!( + TcpListener::bind((Ipv6Addr::LOCALHOST, port)), + Err(ref e) if e.kind() == std::io::ErrorKind::AddrInUse + ) } /// Find an available port, starting from the given port @@ -1604,3 +1625,41 @@ fn main() { process::exit(1); } } + +#[cfg(test)] +mod tests { + use super::is_port_available; + use std::net::{Ipv4Addr, Ipv6Addr, TcpListener}; + + /// A port held only on IPv6 loopback must be reported unavailable. The old + /// IPv4-only probe returned `true` here, which let pg0 collide with an + /// IPv6/dual-stack listener (e.g. Docker's `[::]:5432`). + #[test] + fn ipv6_held_port_is_unavailable() { + let listener = match TcpListener::bind((Ipv6Addr::LOCALHOST, 0)) { + Ok(l) => l, + Err(_) => return, // IPv6 unavailable on this host — skip. + }; + let port = listener.local_addr().unwrap().port(); + assert!(!is_port_available(port), "IPv6-held port should be unavailable"); + } + + /// A port held on IPv4 loopback must be reported unavailable. + #[test] + fn ipv4_held_port_is_unavailable() { + let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + assert!(!is_port_available(port), "IPv4-held port should be unavailable"); + } + + /// A free port (bound then released) is reported available. + #[test] + fn free_port_is_available() { + let port = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)) + .unwrap() + .local_addr() + .unwrap() + .port(); + assert!(is_port_available(port)); + } +}