From 623949f49e5626d1abfa8d53a36cae502438fab8 Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Wed, 22 Apr 2026 23:07:53 +0530 Subject: [PATCH 1/2] fix(websocket): handle IPv6 4-tuple in listener getsockname() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WebsocketListener.listen() unpacked sock.getsockname() directly into (sock_addr, sock_port), which raised ValueError on IPv6 sockets whose getsockname() returns (host, port, flowinfo, scopeid). The ValueError was caught by the outer except and re-raised as OpenConnectionError, so IPv6 WebSocket listeners could not start. Pull the parse into a small _extract_host_port_from_sockname helper that accepts any tuple of length >= 2 with (str, int) in the first two slots, and falls back gracefully on unknown shapes. Add unit tests covering IPv4 2-tuple, IPv6 4-tuple (zero and nonzero scopeid), and unexpected shapes — no actual IPv6 socket needed. Fixes #1316. --- libp2p/transport/websocket/listener.py | 33 ++++++++++++-- newsfragments/1316.bugfix.rst | 6 +++ .../websocket/test_extract_host_port.py | 43 +++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 newsfragments/1316.bugfix.rst create mode 100644 tests/core/transport/websocket/test_extract_host_port.py diff --git a/libp2p/transport/websocket/listener.py b/libp2p/transport/websocket/listener.py index 7a6a210d5..f05b68001 100644 --- a/libp2p/transport/websocket/listener.py +++ b/libp2p/transport/websocket/listener.py @@ -31,6 +31,26 @@ logger = logging.getLogger(__name__) +def _extract_host_port_from_sockname( + sock_name: Any, +) -> tuple[str, int] | None: + """ + Return ``(host, port)`` from a ``socket.getsockname()`` return value, or + ``None`` if the value is not recognised. + + ``socket.getsockname()`` yields a 2-tuple ``(host, port)`` for IPv4 and a + 4-tuple ``(host, port, flowinfo, scopeid)`` for IPv6. Only the first two + fields are meaningful for multiaddr reconstruction, so we accept any + tuple of length ≥ 2 where the first two elements are a string and an int. + """ + if not isinstance(sock_name, tuple) or len(sock_name) < 2: + return None + host, port = sock_name[0], sock_name[1] + if not isinstance(host, str) or not isinstance(port, int): + return None + return host, port + + @dataclass class WebsocketListenerConfig: """Configuration for WebSocket listener.""" @@ -297,9 +317,16 @@ async def _run_server() -> None: if hasattr(server_info, "socket"): sock = server_info.socket if hasattr(sock, "getsockname"): - sock_addr, sock_port = sock.getsockname() - actual_host = sock_addr - actual_port = sock_port + sock_name = sock.getsockname() + host_port = _extract_host_port_from_sockname(sock_name) + if host_port is not None: + actual_host, actual_port = host_port + else: + logger.warning( + "Unexpected getsockname() result %r; " + "falling back to requested host/port", + sock_name, + ) elif hasattr(server_info, "port"): # If we can't get socket, at least get the port actual_port = server_info.port diff --git a/newsfragments/1316.bugfix.rst b/newsfragments/1316.bugfix.rst new file mode 100644 index 000000000..6060566b4 --- /dev/null +++ b/newsfragments/1316.bugfix.rst @@ -0,0 +1,6 @@ +Fix ``WebsocketListener.listen()`` binding IPv6 addresses. ``socket.getsockname()`` +returns a 4-tuple ``(host, port, flowinfo, scopeid)`` for IPv6 sockets, which +previously failed to unpack into ``(sock_addr, sock_port)`` and surfaced as +``OpenConnectionError("Failed to listen on ...")``. The listener now accepts +any 2-or-more-tuple with ``(host: str, port: int)`` in the first two slots and +falls back to the requested host/port on unknown shapes. diff --git a/tests/core/transport/websocket/test_extract_host_port.py b/tests/core/transport/websocket/test_extract_host_port.py new file mode 100644 index 000000000..802ea96de --- /dev/null +++ b/tests/core/transport/websocket/test_extract_host_port.py @@ -0,0 +1,43 @@ +""" +Unit tests for ``_extract_host_port_from_sockname``. + +Historically ``WebsocketListener.listen`` unpacked ``sock.getsockname()`` +as a 2-tuple, which worked for IPv4 sockets but failed with +``ValueError: too many values to unpack`` for IPv6 sockets (whose +``getsockname()`` returns a 4-tuple ``(host, port, flowinfo, scopeid)``). +This regression test pins the supported shapes for the extractor so the +IPv6 path doesn't re-break. +""" + +from __future__ import annotations + +from libp2p.transport.websocket.listener import _extract_host_port_from_sockname + + +def test_ipv4_two_tuple() -> None: + assert _extract_host_port_from_sockname(("127.0.0.1", 12345)) == ( + "127.0.0.1", + 12345, + ) + + +def test_ipv6_four_tuple() -> None: + # (host, port, flowinfo, scopeid) + assert _extract_host_port_from_sockname(("::1", 23456, 0, 0)) == ("::1", 23456) + + +def test_ipv6_four_tuple_with_nonzero_scopeid() -> None: + assert _extract_host_port_from_sockname(("fe80::1", 34567, 0, 3)) == ( + "fe80::1", + 34567, + ) + + +def test_unexpected_shape_returns_none() -> None: + # Not a tuple, single element, wrong element types — all graceful. + assert _extract_host_port_from_sockname(None) is None + assert _extract_host_port_from_sockname(("127.0.0.1",)) is None + assert _extract_host_port_from_sockname((12345, "127.0.0.1")) is None + # A raw string (e.g. what an AF_UNIX socket's getsockname returns) is + # not a tuple and must be rejected. + assert _extract_host_port_from_sockname("socket-path") is None From cc5813bf056cb8f4a9809e91555f78eb02ec7c4f Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Thu, 23 Apr 2026 18:26:53 +0530 Subject: [PATCH 2/2] address Copilot review on #1317 - _extract_host_port_from_sockname now takes sock_name: object rather than Any, matching the runtime isinstance checks and avoiding an unnecessary typing.Any dependency - The sockname-unrecognised warning branch now explicitly restores actual_host/actual_port from the requested host/port, so the 'falling back to requested host/port' log line is self-contained and the intent is obvious from the code flow --- libp2p/transport/websocket/listener.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libp2p/transport/websocket/listener.py b/libp2p/transport/websocket/listener.py index f05b68001..2549eea53 100644 --- a/libp2p/transport/websocket/listener.py +++ b/libp2p/transport/websocket/listener.py @@ -32,7 +32,7 @@ def _extract_host_port_from_sockname( - sock_name: Any, + sock_name: object, ) -> tuple[str, int] | None: """ Return ``(host, port)`` from a ``socket.getsockname()`` return value, or @@ -322,6 +322,9 @@ async def _run_server() -> None: if host_port is not None: actual_host, actual_port = host_port else: + # Explicitly restore the requested host/port so the + # fallback mentioned in the warning is self-contained. + actual_host, actual_port = host, port logger.warning( "Unexpected getsockname() result %r; " "falling back to requested host/port",