Skip to content

Commit 046a52d

Browse files
authored
Merge pull request libp2p#1153 from imApoorva36/fix/listener-exception-handling
fix(swarm): prevent listener crash on inbound upgrade failure
2 parents 4c854ce + d179d6d commit 046a52d

File tree

3 files changed

+105
-6
lines changed

3 files changed

+105
-6
lines changed

libp2p/network/swarm.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -653,10 +653,26 @@ async def conn_handler(
653653
return
654654

655655
raw_conn = RawConnection(read_write_closer, False)
656-
await self.upgrade_inbound_raw_conn(raw_conn, maddr)
657-
# NOTE: This is a intentional barrier to prevent from the handler
658-
# exiting and closing the connection.
659-
await self.manager.wait_finished()
656+
try:
657+
await self.upgrade_inbound_raw_conn(raw_conn, maddr)
658+
# NOTE: This is an intentional barrier to prevent the handler from
659+
# exiting and closing the connection.
660+
await self.manager.wait_finished()
661+
except SwarmException as error:
662+
# Log the error but don't propagate - this prevents listener crash
663+
logger.debug(
664+
"connection handler failed to upgrade connection from %s: %s",
665+
maddr,
666+
error,
667+
)
668+
await read_write_closer.close()
669+
except Exception:
670+
# Catch any other unexpected errors to prevent listener crash
671+
logger.exception(
672+
"unexpected error in connection handler for %s",
673+
maddr,
674+
)
675+
await read_write_closer.close()
660676

661677
try:
662678
# Success
@@ -776,8 +792,13 @@ async def upgrade_inbound_raw_conn(
776792
# Let add_conn perform final guard if needed
777793
pass
778794

779-
await self.add_conn(muxed_conn)
780-
logger.debug("successfully opened connection to peer %s", peer_id)
795+
try:
796+
await self.add_conn(muxed_conn)
797+
logger.debug("successfully opened connection to peer %s", peer_id)
798+
except Exception:
799+
logger.exception("failed to add connection for peer %s", peer_id)
800+
await muxed_conn.close()
801+
return None # type: ignore[return-value]
781802

782803
return muxed_conn
783804

newsfragments/417.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed swarm listener crash on inbound peer negotiation failures by handling security and muxer upgrade exceptions gracefully, allowing the listener to continue accepting new connections.

tests/core/network/test_swarm.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,83 @@ async def test_swarm_listen_multiple_addresses_connectivity(security_protocol):
417417
)
418418

419419

420+
@pytest.mark.trio
421+
async def test_swarm_listener_resilience_on_upgrade_failure(security_protocol):
422+
"""Test that the swarm listener continues to accept connections even when individual peer negotiations fail.""" # noqa: E501
423+
from unittest.mock import patch
424+
425+
from libp2p.transport.exceptions import SecurityUpgradeFailure
426+
427+
async with SwarmFactory.create_batch_and_listen(
428+
2, security_protocol=security_protocol
429+
) as swarms:
430+
listener_swarm = swarms[0]
431+
client_swarm = swarms[1]
432+
433+
# Get listener address
434+
listener_addr = tuple(
435+
addr
436+
for transport in listener_swarm.listeners.values()
437+
for addr in transport.get_addrs()
438+
)[0]
439+
440+
# Add the listener's address to client's peerstore
441+
client_swarm.peerstore.add_addrs(
442+
listener_swarm.get_peer_id(), [listener_addr], 10000
443+
)
444+
445+
# First, establish a successful connection to verify setup
446+
await client_swarm.dial_peer(listener_swarm.get_peer_id())
447+
assert listener_swarm.get_peer_id() in client_swarm.connections
448+
449+
# Close the first connection
450+
await client_swarm.close_peer(listener_swarm.get_peer_id())
451+
await trio.sleep(0.1)
452+
453+
# Now simulate a failure during upgrade by patching upgrade_security
454+
# We'll make one call fail, then let subsequent calls succeed
455+
original_upgrade_security = listener_swarm.upgrader.upgrade_security
456+
call_count = [0]
457+
458+
async def failing_upgrade_security(*args, **kwargs):
459+
call_count[0] += 1
460+
if call_count[0] == 1:
461+
# First call after patch fails
462+
raise SecurityUpgradeFailure("Simulated security upgrade failure")
463+
# Subsequent calls succeed
464+
return await original_upgrade_security(*args, **kwargs)
465+
466+
with patch.object(
467+
listener_swarm.upgrader,
468+
"upgrade_security",
469+
side_effect=failing_upgrade_security,
470+
):
471+
# This connection attempt should fail on the listener side
472+
# The client will see a connection error, but the listener should NOT crash
473+
try:
474+
await client_swarm.dial_peer(listener_swarm.get_peer_id())
475+
except Exception:
476+
# Connection failure expected - client can't complete the dial
477+
pass
478+
479+
# Give time for any potential listener crash to occur
480+
await trio.sleep(0.2)
481+
482+
# Verify listener is still active by checking it has listeners
483+
assert len(listener_swarm.listeners) > 0, (
484+
"Listener crashed after upgrade failure!"
485+
)
486+
487+
# Now verify the listener can still accept new connections
488+
# (upgrade_security is no longer patched, so this should succeed)
489+
await trio.sleep(0.1)
490+
await client_swarm.dial_peer(listener_swarm.get_peer_id())
491+
492+
# Verify connection was established successfully
493+
assert listener_swarm.get_peer_id() in client_swarm.connections
494+
assert client_swarm.get_peer_id() in listener_swarm.connections
495+
496+
420497
@pytest.mark.trio
421498
async def test_swarm_peer_id_validation(security_protocol):
422499
"""Test that the swarm correctly validates peer IDs during connection."""

0 commit comments

Comments
 (0)