Fix IPv6 host bracketing in WebSocket transport

This commit is contained in:
GautamBytes
2025-07-23 08:10:51 +00:00
parent 8a21435fc4
commit 1997777c52
6 changed files with 73 additions and 31 deletions

View File

@ -38,7 +38,10 @@ class WebsocketListener(IListener):
or maddr.value_for_protocol("dns6") or maddr.value_for_protocol("dns6")
or "0.0.0.0" or "0.0.0.0"
) )
port = int(maddr.value_for_protocol("tcp")) port_str = maddr.value_for_protocol("tcp")
if port_str is None:
raise ValueError(f"No TCP port found in multiaddr: {maddr}")
port = int(port_str)
async def serve( async def serve(
task_status: TaskStatus[Any] = trio.TASK_STATUS_IGNORED, task_status: TaskStatus[Any] = trio.TASK_STATUS_IGNORED,

View File

@ -16,24 +16,38 @@ class WebsocketTransport(ITransport):
""" """
async def dial(self, maddr: Multiaddr) -> RawConnection: async def dial(self, maddr: Multiaddr) -> RawConnection:
text = str(maddr) # Handle addresses with /p2p/ PeerID suffix by truncating them at /ws
if text.endswith("/wss"): addr_text = str(maddr)
try:
ws_part_index = addr_text.index("/ws")
# Create a new Multiaddr containing only the transport part
transport_maddr = Multiaddr(addr_text[: ws_part_index + 3])
except ValueError:
raise ValueError(
f"WebsocketTransport requires a /ws protocol, not found in {maddr}"
) from None
# Check for /wss, which is not supported yet
if str(transport_maddr).endswith("/wss"):
raise NotImplementedError("/wss (TLS) not yet supported") raise NotImplementedError("/wss (TLS) not yet supported")
if not text.endswith("/ws"):
raise ValueError(f"WebsocketTransport only supports /ws, got {maddr}")
host = ( host = (
maddr.value_for_protocol("ip4") transport_maddr.value_for_protocol("ip4")
or maddr.value_for_protocol("ip6") or transport_maddr.value_for_protocol("ip6")
or maddr.value_for_protocol("dns") or transport_maddr.value_for_protocol("dns")
or maddr.value_for_protocol("dns4") or transport_maddr.value_for_protocol("dns4")
or maddr.value_for_protocol("dns6") or transport_maddr.value_for_protocol("dns6")
) )
if host is None: if host is None:
raise ValueError(f"No host protocol found in {maddr}") raise ValueError(f"No host protocol found in {transport_maddr}")
port = int(maddr.value_for_protocol("tcp")) port_str = transport_maddr.value_for_protocol("tcp")
uri = f"ws://{host}:{port}" if port_str is None:
raise ValueError(f"No TCP port found in multiaddr: {transport_maddr}")
port = int(port_str)
host_str = f"[{host}]" if ":" in host else host
uri = f"ws://{host_str}:{port}"
try: try:
async with open_websocket_url(uri, ssl_context=None) as ws: async with open_websocket_url(uri, ssl_context=None) as ws:

View File

@ -12,6 +12,8 @@
"dependencies": { "dependencies": {
"@libp2p/ping": "^2.0.36", "@libp2p/ping": "^2.0.36",
"@libp2p/websockets": "^9.2.18", "@libp2p/websockets": "^9.2.18",
"@chainsafe/libp2p-yamux": "^5.0.1",
"@libp2p/plaintext": "^2.0.7",
"libp2p": "^2.9.0", "libp2p": "^2.9.0",
"multiaddr": "^10.0.1" "multiaddr": "^10.0.1"
} }

View File

@ -1,20 +1,20 @@
import { createLibp2p } from 'libp2p' import { createLibp2p } from 'libp2p'
import { webSockets } from '@libp2p/websockets' import { webSockets } from '@libp2p/websockets'
import { ping } from '@libp2p/ping' import { ping } from '@libp2p/ping'
import { plaintext } from '@libp2p/insecure' import { plaintext } from '@libp2p/plaintext'
import { mplex } from '@libp2p/mplex' import { yamux } from '@chainsafe/libp2p-yamux'
async function main() { async function main() {
const node = await createLibp2p({ const node = await createLibp2p({
transports: [ webSockets() ], transports: [ webSockets() ],
connectionEncryption: [ plaintext() ], connectionEncryption: [ plaintext() ],
streamMuxers: [ mplex() ], streamMuxers: [ yamux() ],
services: { services: {
// installs /ipfs/ping/1.0.0 handler // installs /ipfs/ping/1.0.0 handler
ping: ping() ping: ping()
}, },
addresses: { addresses: {
listen: ['/ip4/127.0.0.1/tcp/0/ws'] listen: ['/ip4/0.0.0.0/tcp/0/ws']
} }
}) })

View File

@ -10,12 +10,13 @@ from trio.lowlevel import open_process
from libp2p.crypto.secp256k1 import create_new_key_pair from libp2p.crypto.secp256k1 import create_new_key_pair
from libp2p.custom_types import TProtocol from libp2p.custom_types import TProtocol
from libp2p.host.basic_host import BasicHost from libp2p.host.basic_host import BasicHost
from libp2p.network.exceptions import SwarmException
from libp2p.network.swarm import Swarm from libp2p.network.swarm import Swarm
from libp2p.peer.id import ID from libp2p.peer.id import ID
from libp2p.peer.peerinfo import PeerInfo from libp2p.peer.peerinfo import PeerInfo
from libp2p.peer.peerstore import PeerStore from libp2p.peer.peerstore import PeerStore
from libp2p.security.insecure.transport import InsecureTransport from libp2p.security.insecure.transport import InsecureTransport
from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex from libp2p.stream_muxer.yamux.yamux import Yamux
from libp2p.transport.upgrader import TransportUpgrader from libp2p.transport.upgrader import TransportUpgrader
from libp2p.transport.websocket.transport import WebsocketTransport from libp2p.transport.websocket.transport import WebsocketTransport
@ -24,10 +25,20 @@ PLAINTEXT_PROTOCOL_ID = "/plaintext/1.0.0"
@pytest.mark.trio @pytest.mark.trio
async def test_ping_with_js_node(): async def test_ping_with_js_node():
# Path to the JS node script
js_node_dir = os.path.join(os.path.dirname(__file__), "js_libp2p", "js_node", "src") js_node_dir = os.path.join(os.path.dirname(__file__), "js_libp2p", "js_node", "src")
script_name = "./ws_ping_node.mjs" script_name = "./ws_ping_node.mjs"
try:
subprocess.run(
["npm", "install"],
cwd=js_node_dir,
check=True,
capture_output=True,
text=True,
)
except (subprocess.CalledProcessError, FileNotFoundError) as e:
pytest.fail(f"Failed to run 'npm install': {e}")
# Launch the JS libp2p node (long-running) # Launch the JS libp2p node (long-running)
proc = await open_process( proc = await open_process(
["node", script_name], ["node", script_name],
@ -35,22 +46,25 @@ async def test_ping_with_js_node():
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
cwd=js_node_dir, cwd=js_node_dir,
) )
assert proc.stdout is not None, "stdout pipe missing"
assert proc.stderr is not None, "stderr pipe missing"
stdout = proc.stdout
stderr = proc.stderr
try: try:
# Read first two lines (PeerID and multiaddr) # Read first two lines (PeerID and multiaddr)
buffer = b"" buffer = b""
with trio.fail_after(10): with trio.fail_after(30):
while buffer.count(b"\n") < 2: while buffer.count(b"\n") < 2:
chunk = await proc.stdout.receive_some(1024) # type: ignore chunk = await stdout.receive_some(1024)
if not chunk: if not chunk:
break break
buffer += chunk buffer += chunk
# Split and filter out any empty lines
lines = [line for line in buffer.decode().splitlines() if line.strip()] lines = [line for line in buffer.decode().splitlines() if line.strip()]
if len(lines) < 2: if len(lines) < 2:
stderr_output = "" stderr_output = await stderr.receive_some(2048)
if proc.stderr is not None: stderr_output = stderr_output.decode()
stderr_output = (await proc.stderr.receive_some(2048)).decode()
pytest.fail( pytest.fail(
"JS node did not produce expected PeerID and multiaddr.\n" "JS node did not produce expected PeerID and multiaddr.\n"
f"Stdout: {buffer.decode()!r}\n" f"Stdout: {buffer.decode()!r}\n"
@ -70,7 +84,7 @@ async def test_ping_with_js_node():
secure_transports_by_protocol={ secure_transports_by_protocol={
TProtocol(PLAINTEXT_PROTOCOL_ID): InsecureTransport(key_pair) TProtocol(PLAINTEXT_PROTOCOL_ID): InsecureTransport(key_pair)
}, },
muxer_transports_by_protocol={TProtocol(MPLEX_PROTOCOL_ID): Mplex}, muxer_transports_by_protocol={TProtocol("/yamux/1.0.0"): Yamux},
) )
transport = WebsocketTransport() transport = WebsocketTransport()
swarm = Swarm(py_peer_id, peer_store, upgrader, transport) swarm = Swarm(py_peer_id, peer_store, upgrader, transport)
@ -78,9 +92,19 @@ async def test_ping_with_js_node():
# Connect to JS node # Connect to JS node
peer_info = PeerInfo(peer_id, [maddr]) peer_info = PeerInfo(peer_id, [maddr])
await host.connect(peer_info)
await trio.sleep(1)
try:
await host.connect(peer_info)
except SwarmException as e:
underlying_error = e.__cause__
pytest.fail(
"Connection failed with SwarmException.\n"
f"THE REAL ERROR IS: {underlying_error!r}\n"
)
assert host.get_network().connections.get(peer_id) is not None assert host.get_network().connections.get(peer_id) is not None
await trio.sleep(0.1)
# Ping protocol # Ping protocol
stream = await host.new_stream(peer_id, [TProtocol("/ipfs/ping/1.0.0")]) stream = await host.new_stream(peer_id, [TProtocol("/ipfs/ping/1.0.0")])
@ -88,7 +112,6 @@ async def test_ping_with_js_node():
data = await stream.read(4) data = await stream.read(4)
assert data == b"pong" assert data == b"pong"
# Cleanup
await host.close() await host.close()
finally: finally:
proc.send_signal(signal.SIGTERM) proc.send_signal(signal.SIGTERM)

View File

@ -12,7 +12,7 @@ from libp2p.peer.id import ID
from libp2p.peer.peerinfo import PeerInfo from libp2p.peer.peerinfo import PeerInfo
from libp2p.peer.peerstore import PeerStore from libp2p.peer.peerstore import PeerStore
from libp2p.security.insecure.transport import InsecureTransport from libp2p.security.insecure.transport import InsecureTransport
from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex from libp2p.stream_muxer.yamux.yamux import Yamux
from libp2p.transport.upgrader import TransportUpgrader from libp2p.transport.upgrader import TransportUpgrader
from libp2p.transport.websocket.transport import WebsocketTransport from libp2p.transport.websocket.transport import WebsocketTransport
@ -33,7 +33,7 @@ async def make_host(
secure_transports_by_protocol={ secure_transports_by_protocol={
TProtocol(PLAINTEXT_PROTOCOL_ID): InsecureTransport(key_pair) TProtocol(PLAINTEXT_PROTOCOL_ID): InsecureTransport(key_pair)
}, },
muxer_transports_by_protocol={TProtocol(MPLEX_PROTOCOL_ID): Mplex}, muxer_transports_by_protocol={TProtocol("/yamux/1.0.0"): Yamux},
) )
# Transport + Swarm + Host # Transport + Swarm + Host