Feat/multi transport support#1357
Conversation
Implements go-libp2p-style multi-transport architecture so a single Host
can listen on and dial over TCP, WebSocket, and QUIC at the same time.
Core changes
------------
* libp2p/abc.py — Extended ITransport: add can_dial(), can_listen(),
protocols() to the interface.
* libp2p/transport/manager.py (NEW)
— TransportManager: two-step routing via protocol-name
pre-filter + can_dial/can_listen; robust getattr
fallbacks for test fakes.
* libp2p/network/swarm.py
— Swarm.__init__ now accepts transports: list[ITransport];
listen() and _dial_addr_single_attempt() route via
TransportManager; QUIC inbound handled via
isinstance(rwc, IMuxedConn) not a class check;
deprecated swarm.transport property retained.
* libp2p/__init__.py — new_swarm() / new_host() auto-detect required
transports from listen_addrs via transport registry
(module-level import for monkeypatch compat);
explicit transports= list is highest priority;
quic_class= passed at call-site so tests can patch
libp2p.QUICTransport.
* libp2p/transport/tcp/tcp.py — Add can_dial, can_listen, protocols
* libp2p/transport/websocket/transport.py
— can_dial sync (remove spurious await),
add can_listen, protocols
* libp2p/transport/quic/transport.py — Add can_listen, protocols returns list[str]
Tests
-----
* tests/core/transport/test_transport_manager.py (NEW) — 26 unit tests
* Full regression: 529 passed, 2 skipped, 0 failed
Documentation / examples
-------------------------
* examples/multi_transport/server.py — listen on all three simultaneously
* examples/multi_transport/client.py — auto-selects transport from multiaddr
* docs/examples.multi_transport.rst — API reference + migration guide
* docs/examples.rst — added to toctree
* newsfragments/1345.feature.rst
* README.md — Multi-Transport section under Transports
- Refactored TransportManager to handle multi-port CMUX shared port bindings - Fixed interface contract violations in Swarm.py listener handling - Deprecated TransportRegistry in favor of TransportManager single-source-of-truth - Resolved PeekableStream/PortDemultiplexer race conditions using proper close callbacks - Implemented Happy Eyeballs dial routing for Swarm.dial_peer - Implemented correct listen_order priority sorting in TransportManager.add_transport - Automatically register CircuitV2Transport in Swarm creation when relay is enabled
…multi_transport_support
|
Issue # ? |
…e project excludes
…arm and init files
… address handling in TransportManager
ec8cde6 to
aaa41d7
Compare
- Prevent event loop blocking in client.py by replacing time.sleep with trio.sleep. - Refactor _start_one in swarm.py to remove duplicated closure logic and use _handle_inbound_connection. - Fix type hint drift in DemultiplexedListener and demultiplexed_listen in cmux.py.
…host dead code - Swarm.__init__ now gracefully handles legacy 'transport=' keyword by throwing a clear TypeError - Removed unnecessary ValueError raising when instantiating Swarm without transports - Removed unreachable dead code regarding legacy transport negotiation in basic_host - Added newsfragment to explicitly document the intentional removal of the transport_registry and legacy transport argument
|
Hi @sumanjeet0012 Thanks for this PR and multi transport support. This was and old TODO. FULL REVIEW: AI PR Review: #1357 — Feat/multi transport supportPR: #1357 1. Summary of ChangesThis PR is a feature enhancement that closes issue #1359. It enables a single Problem addressed (#1359)Previously, Core changes
Breaking changes / deprecations
Maintainer feedback status@acul71 commented "Issue # ?" on 2026-06-28. The PR body now includes 2. Branch Sync Status and Merge ConflictsBranch Sync Status
Merge Conflict Analysis✅ No merge conflicts detected. Test merge against 3. Strengths
4. Issues FoundCriticalNone identified. The full test suite passes locally (3057 tests) and the core routing logic is well-covered. Major
if kwargs.pop("transport", None):
raise TypeError(
"Swarm() no longer accepts 'transport='. Use transports=[...] instead."
)
_can_dial = getattr(transport, "can_dial", None)
if _can_dial is None or _can_dial(maddr):
logger.debug(
Minor
except Exception:
pass
except BaseException:
pass
5. Security Review
No authentication bypass, key handling, or input-validation regressions identified in the transport routing layer. 6. Documentation and Examples
The examples are useful, but user-facing documentation does not yet explain the breaking removal of 7. Newsfragment Requirement
Action required before approval:
8. Tests and ValidationLinting (
|
| Flow | Behavior in multi-transport scenario | Assessment |
|---|---|---|
| Inbound learning | After connect, Identify's _update_peerstore_from_identify() adds every remote listen_addr (TCP, WS, QUIC, etc.) with a 2-hour TTL |
✅ Correct |
| Outbound dialing | connect() merges peer_info.addrs into peerstore, then dial_peer() reads peerstore.addrs(peer_id) and Happy Eyeballs dials up to 8 addresses in parallel; each addr is routed by TransportManager.transport_for_dialing() |
✅ Correct |
| Self advertisement | BasicHost.get_transport_addrs() iterates all swarm.listeners and merges with observed (NAT) addresses in get_addrs(); Identify sends the full set as listen_addrs |
✅ Correct |
| Test coverage | test_host_connect updated to assert host 0 learns all of host 1's advertised addresses, not just one |
✅ Improved |
Peerstore does not record which transports the local host supports. That is intentional: it caches remote reachability. If peerstore holds a QUIC addr but the local host only registered TCP, dials to that addr fail (logged) until TTL expires — harmless when another addr (e.g. TCP) succeeds via Happy Eyeballs.
Observed addresses / NAT — transport-aware
ObservedAddrManager is well-suited to multi-transport:
- Splits addresses into a thin waist (
/ip4/…/tcp/…or/ip4/…/udp/…) and matches observations per transport family. - Rest suffix inference (
/ws,/quic-v1, etc.) is derived from local listen addresses passed viaget_transport_addrs(). get_nat_type()returns separate TCP and UDP NAT classifications.
Identify passes the full transport address list into record_observation(), so all listen paths participate in NAT discovery.
Swarm connections — multi-path per peer
connections[peer_id]is a list (defaultmax_connections_per_peer = 3), so a node can hold TCP and QUIC connections to the same peer simultaneously.- Happy Eyeballs may open multiple connections until the per-peer limit; older connections are trimmed.
dial_peer()reuses any existing open connection without preferring transport type — usually fine, but means a TCP conn may be reused even when a QUIC addr is also available.
Other subsystems — unchanged model, per-address evaluation
| Subsystem | Multi-transport impact |
|---|---|
| Signed peer records | Transport-independent; records may contain multiple addrs across protocols |
| Connection gate / rcmgr | Evaluated per multiaddr (IP extracted per dial/listen attempt) |
| Circuit relay v2 | Reads peerstore addrs and filters /p2p-circuit paths — unaffected |
| Protocol book (proto book) | Independent of transport layer |
Gaps and watch items
| Area | Status | Notes |
|---|---|---|
| Filter peerstore addrs by local transport capability before dial | ❌ Not implemented | Dial-and-fail is acceptable when another addr works; may add latency/noise in logs |
| Transport-aware connection reuse | ❌ Not implemented | First open connection wins regardless of transport |
| Identify + peerstore with simultaneous TCP/WS/QUIC hosts | test_simultaneous_transports.py covers binding/dialing; no test that Identify populates peerstore with all three addr families from one remote |
|
enable_webrtc without WebRTC listen addrs |
❌ Dead parameter on PR branch | Interop still works because it passes /webrtc-direct in listen_addrs |
Registry removal vs peerstore (supplementary)
The removed transport_registry was a local transport factory for new_swarm(), not a peerstore component. Peerstore was never coupled to it. Auto-detection logic moved into _build_transports_for_swarm(); peerstore interaction paths are unchanged.
Bottom line: Peerstore and the identify/observed-addr pipeline are compatible with multi-transport without structural changes. The PR correctly wires address advertisement and per-address dial routing. Remaining gaps are operational (dialing unsupported addrs, connection reuse policy), not peerstore storage bugs.
10. Recommendations for Improvement
- Align backward-compat story: Update PR description,
1359.feature.rst, and code so they agree on whethertransport=is deprecated (warning) or removed (TypeError). Consider emittingDeprecationWarningfor positional single-transport for one release. - Fix newsfragment #1360: Open a tracking issue for the
transport_registryremoval or merge breaking notes into Enhancement: Multi transport capability #1359 with1359.breaking.rst. - Reply to @acul71 confirming issue Enhancement: Multi transport capability #1359 and scope.
- Add migration documentation beyond automodule stubs — especially for removed
transport_registryand changedSwarmconstructor. - Add API regression tests for
Swarm(transport=...)TypeError and positional/listtransportsforms. - Tighten TransportManager routing — drop
can_dial is Nonefallback or warn loudly. - Squash commits before merge per author To-Do and reviewer ergonomics.
- Add an Identify + peerstore integration test where a multi-transport host advertises TCP, WS, and QUIC addrs and a connecting peer's peerstore receives all of them after Identify.
11. Questions for the Author
- Was the intentional decision to raise
TypeErrorforSwarm(transport=...)rather than emitDeprecationWarningas documented in the PR body and1359.feature.rst? - Should the breaking removal of
transport_registrybe tracked under Enhancement: Multi transport capability #1359 or a separate issue? If separate, can you open it so1360.breaking.rstis valid? - Is shared-port TCP + WebSocket dialing (client connects to
/tcp/Nwhile server also has/tcp/N/ws) covered by integration tests beyond listener binding? A cross-transport echo test on the same port would increase confidence. - Why was the
swarm.transportdeprecated property removed rather than retained as an alias totransport_manager.get_transports()[0]? - Will the commit history be squashed before merge, as noted in the PR To-Do?
- Should
dial_peer()prefer opening a connection on a specific transport when peerstore holds multiple addr families, or is "first open connection wins" the intended go-libp2p-aligned behavior?
12. Overall Assessment
| Metric | Rating |
|---|---|
| Quality Rating | Good — solid architectural alignment with go-libp2p, well-tested core routing, clean local CI |
| Security Impact | Low |
| Merge Readiness | Needs fixes — newsfragment validity/contradiction and maintainer reply are blockers; documentation gaps should be addressed |
| Confidence | High — full local test/lint/typecheck/docs run on checked-out branch; code review of transport manager, cmux, and swarm integration |
This is a valuable, well-engineered feature that addresses a real gap in py-libp2p. The implementation and test coverage for multi-transport routing are strong. Before merge, the project should resolve the conflicting backward-compat documentation, fix the invalid 1360.breaking.rst newsfragment, respond to maintainer feedback, and flesh out user-facing migration docs.
|
Thanks @sumanjeet0012 — reviewed the latest commit ( Addressed in
Still open (non-blocking unless noted)
Local spot-check on the new tests passed. Happy to re-review after any follow-up. |
|
@acul71 I have wired enable_webrtc and added tests. |
|
Thanks @sumanjeet0012 — Looks good in
Together with Small nits before merge (non-blocking)
CI was green on lint/docs/interop; core matrix was still running when I last checked. Nice work on this — very close from my side once the above hygiene is done. |
|
@acul71 PR description does not mention DeprecationWarning, its already removed. |
|
@sumanjeet0012 , @acul71 : This is an outstanding contribution that significantly advances py-libp2p's networking capabilities. The multi-transport architecture is well designed, aligns closely with the broader libp2p ecosystem, and introduces a clean, maintainable TransportManager abstraction while simplifying the overall transport layer. Appreciate the thorough testing, comprehensive documentation updates, and the attention given to backward compatibility and migration guidance. The implementation is clean, well-structured, and backed by a strong CI matrix, giving confidence in its correctness and long-term maintainability. Overall, this is a high-quality contribution that closes a long-standing feature gap and provides a solid foundation for future transport enhancements. @sumanjeet0012 : Excellent work, and thank you for your persistence and collaboration throughout the review process with @acul71. CCing @mishmosh and @johannamoran |
|
@sumanjeet0012 , @acul71 : Please open a detailed discussion page at py-libp2p on this PR. Overtime, this will be further scaled up and be made available as spec addition at libp2p. |
Closes #1359
Description
This PR introduces multi-transport support, bringing
py-libp2pcloser togo-libp2p'sTransportManagerarchitecture. A node can now listen and dial over TCP, WebSocket, and QUIC simultaneously.Key Features & Changes
transports=[...]list tonew_swarmornew_host, or allow transports to be automatically detected fromlisten_addrs.TransportManagerto route dials and listens to the correct transport seamlessly.PortDemultiplexerallows multiple transports (e.g., TCP and WebSockets) to securely share the same underlying OS port by inspecting incoming byte streams.trio.CancelledandKeyboardInterruptevents aren't swallowed.Breaking Changes⚠️
transport_registry: Thelibp2p.transport.transport_registrymodule has been completely removed. Transports should now be explicitly registered viaTransportManageror passed as a list to the swarm.transport=keyword: TheSwarm.__init__transport=argument is gone. Attempting to pass a single transport via this keyword will now actively raise aTypeErrorexplaining how to migrate totransports=[...].Documentation
docs/examples.multi_transport.rst.1359.breaking.rstnewsfragment to officially document thetransport=keyword removal and registry deprecation.