Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
}