Fix dual-stack socket handling: IPV6_V6ONLY, IPv4-first, is_port_available all-family check (#20643)
This commit is contained in:
@@ -765,8 +765,16 @@ def _get_addrinfos_for_bind(host=None, port=0):
|
||||
0,
|
||||
socket.AI_ADDRCONFIG | socket.AI_PASSIVE,
|
||||
)
|
||||
seen = set()
|
||||
return [i for i in infos if i[0] not in seen and not seen.add(i[0])]
|
||||
deduped = []
|
||||
seen_families = set()
|
||||
for info in infos:
|
||||
if info[0] not in seen_families:
|
||||
seen_families.add(info[0])
|
||||
deduped.append(info)
|
||||
# Prefer IPv4 so that callers without an explicit host get consistent
|
||||
# behaviour across platforms (some OSes list IPv6 first).
|
||||
deduped.sort(key=lambda x: (x[0] != socket.AF_INET,))
|
||||
return deduped
|
||||
except socket.gaierror:
|
||||
fallback_host = "0.0.0.0" if host is None else host
|
||||
return [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (fallback_host, port))]
|
||||
@@ -794,22 +802,31 @@ def try_bind_socket(host=None, port=0, *, reuse_addr=True, listen=False):
|
||||
for family, socktype, proto, _, sockaddr in _get_addrinfos_for_bind(host, port):
|
||||
sock = socket.socket(family, socktype, proto)
|
||||
try:
|
||||
if family == socket.AF_INET6:
|
||||
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
||||
if reuse_addr:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(sockaddr)
|
||||
if listen:
|
||||
sock.listen(1)
|
||||
return sock
|
||||
except OSError:
|
||||
except (OSError, OverflowError):
|
||||
sock.close()
|
||||
raise OSError(f"Could not bind port {port} on any configured address family")
|
||||
|
||||
|
||||
def is_port_available(port):
|
||||
"""Return whether a port is available."""
|
||||
"""Return whether a port is available on all configured address families."""
|
||||
try:
|
||||
sock = try_bind_socket(port=port, listen=True)
|
||||
sock.close()
|
||||
for family, socktype, proto, _, sockaddr in _get_addrinfos_for_bind(port=port):
|
||||
sock = socket.socket(family, socktype, proto)
|
||||
try:
|
||||
if family == socket.AF_INET6:
|
||||
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(sockaddr)
|
||||
finally:
|
||||
sock.close()
|
||||
return True
|
||||
except (OSError, OverflowError):
|
||||
return False
|
||||
@@ -2695,13 +2712,10 @@ def get_open_port() -> int:
|
||||
if port is not None:
|
||||
port = int(port)
|
||||
while True:
|
||||
try:
|
||||
sock = try_bind_socket(port=port, reuse_addr=False)
|
||||
sock.close()
|
||||
if is_port_available(port):
|
||||
return port
|
||||
except OSError:
|
||||
logger.info("Port %d is already in use, trying port %d", port, port + 1)
|
||||
port += 1
|
||||
logger.info("Port %d is already in use, trying port %d", port, port + 1)
|
||||
port += 1
|
||||
sock = try_bind_socket()
|
||||
port = sock.getsockname()[1]
|
||||
sock.close()
|
||||
|
||||
Reference in New Issue
Block a user