diff --git a/trio/_highlevel_open_tcp_listeners.py b/trio/_highlevel_open_tcp_listeners.py index 8d8762170f..d3e3dc4f58 100644 --- a/trio/_highlevel_open_tcp_listeners.py +++ b/trio/_highlevel_open_tcp_listeners.py @@ -48,27 +48,31 @@ async def open_tcp_listeners(port, *, host=None, backlog=None): Args: - port (int): The port to listen on. If you pass 0, the kernel will - automatically pick an arbitrary open port. But be careful: if you - use ``port=0`` when binding to multiple IP address, then each IP - address will be assigned a different port. An example of when this - happens is when ``host=None``, which means to bind to both the IPv4 - wildcard address (``0.0.0.0``) and also the IPv6 wildcard address - (``::``). + port (int or None): The port to listen on. If you pass ``None`` or 0, + the kernel will automatically pick an arbitrary open port. (It + doesn't matter whether you pass ``None`` or 0; they're exactly + equivalent.) But be careful: if you use this feature when binding to + multiple IP addresses, then the random port selection will be done + separately for each IP address, so the returned listeners will + probably be listening on different ports. One situation where this + commonly occurs is when ``host=None`` (the default), which means to + bind to both the IPv4 wildcard address (``0.0.0.0``) and also the + IPv6 wildcard address (``::``). host (str or None): The local interface to bind to. This is passed to :func:`~socket.getaddrinfo` with the ``AI_PASSIVE`` flag set. + If you want to bind to bind to the wildcard address on both IPv4 and + IPv6, in order to accept connections on all available interfaces + then, pass ``None``. This is the default. + If you have a specific interface you want to bind to, pass its IP address or hostname here. If a hostname resolves to multiple IP - addresses, this function will bind one listener to each of them. - - If you want to bind to all available interfaces (the wildcard - address) for both IPv4 and IPv6, pass ``None`` (the default). + addresses, this function will one listener to each of them. If you want to use only IPv4, or only IPv6, but want to accept on all interfaces, pass the family-specific wildcard address: - ``"0.0.0.0"`` or ``"::"``. + ``"0.0.0.0"`` or ``"::"``, respectively. backlog (int or None): The listen backlog to use. If you leave this as ``None`` then Trio will pick a good default. diff --git a/trio/_highlevel_ssl_helpers.py b/trio/_highlevel_ssl_helpers.py index d39ea63c93..88362c8a61 100644 --- a/trio/_highlevel_ssl_helpers.py +++ b/trio/_highlevel_ssl_helpers.py @@ -74,7 +74,8 @@ async def open_ssl_over_tcp_listeners( """Start listening for SSL/TLS-encrypted TCP connections to the given port. Args: - port (int): The port to listen on. See :func:`open_tcp_listeners`. + port (int or None): The port to listen on. See + :func:`open_tcp_listeners`. ssl_context (~ssl.SSLContext): The SSL context to use for all incoming connections. host (str or None): The address to bind to; use ``None`` to bind to the diff --git a/trio/_socket.py b/trio/_socket.py index d7e3b7acf0..c97883f02a 100644 --- a/trio/_socket.py +++ b/trio/_socket.py @@ -210,6 +210,10 @@ async def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): """ + # Work around https://bugs.python.org/issue31198 + if port is None: + port = 0 + # If host and port are numeric, then getaddrinfo doesn't block and we can # skip the whole thread thing, which seems worthwhile. So we try first # with the _NUMERIC_ONLY flags set, and then only spawn a thread if that diff --git a/trio/tests/test_highlevel_open_tcp_listeners.py b/trio/tests/test_highlevel_open_tcp_listeners.py index 995295ba1b..84b11257da 100644 --- a/trio/tests/test_highlevel_open_tcp_listeners.py +++ b/trio/tests/test_highlevel_open_tcp_listeners.py @@ -12,7 +12,7 @@ async def test_open_tcp_listeners_basic(): - listeners = await open_tcp_listeners(0) + listeners = await open_tcp_listeners(None) assert isinstance(listeners, list) for obj in listeners: assert isinstance(obj, SocketListener) @@ -77,7 +77,7 @@ async def measure_backlog(listener): async def test_open_tcp_listeners_backlog(): # Operating systems don't necessarily use the exact backlog you pass async def check_backlog(nominal, required_min, required_max): - listeners = await open_tcp_listeners(0, backlog=nominal) + listeners = await open_tcp_listeners(None, backlog=nominal) actual = await measure_backlog(listeners[0]) for listener in listeners: await listener.aclose() @@ -90,7 +90,7 @@ async def check_backlog(nominal, required_min, required_max): async def test_open_tcp_listeners_ipv6_v6only(): # Check IPV6_V6ONLY is working properly - (ipv6_listener,) = await open_tcp_listeners(0, host="::1") + (ipv6_listener,) = await open_tcp_listeners(None, host="::1") _, port, *_ = ipv6_listener.socket.getsockname() with pytest.raises(OSError): @@ -98,7 +98,7 @@ async def test_open_tcp_listeners_ipv6_v6only(): async def test_open_tcp_listeners_rebind(): - (l1,) = await open_tcp_listeners(0, host="127.0.0.1") + (l1,) = await open_tcp_listeners(None, host="127.0.0.1") sockaddr1 = l1.socket.getsockname() # Plain old rebinding while it's still there should fail, even if we have @@ -210,3 +210,12 @@ async def test_open_tcp_listeners_multiple_host_cleanup_on_error(): assert len(fsf.sockets) == 3 for sock in fsf.sockets: assert sock.closed + + +async def test_open_tcp_listeners_accepts_port_None_and_0(): + for port in [None, 0]: + # https://bugs.python.org/issue31198 + for host in ["127.0.0.1", None]: + listeners = await open_tcp_listeners(port, host=host) + for l in listeners: + await l.aclose() diff --git a/trio/tests/test_socket.py b/trio/tests/test_socket.py index 03d4e6d330..34b34fb665 100644 --- a/trio/tests/test_socket.py +++ b/trio/tests/test_socket.py @@ -154,6 +154,17 @@ def without_proto(gai_tup): await tsocket.getaddrinfo("asdf", "12345") +async def test_getaddrinfo_port_None(): + # https://bugs.python.org/issue31198 + res = await tsocket.getaddrinfo("127.0.0.1", None) + assert res[0][-1] == ("127.0.0.1", 0) + + res = await tsocket.getaddrinfo( + None, None, family=tsocket.AF_INET, flags=tsocket.AI_PASSIVE + ) + assert res[0][-1] == ("0.0.0.0", 0) + + async def test_getnameinfo(): # Trivial test: ni_numeric = stdlib_socket.NI_NUMERICHOST | stdlib_socket.NI_NUMERICSERV