Skip to content

feat(webrtc): add UdpMux for shared-port WebRTC-Direct inbound dispatch#1381

Open
yashksaini-coder wants to merge 2 commits into
libp2p:mainfrom
yashksaini-coder:worktree-feat+webrtc-udpmux-1352
Open

feat(webrtc): add UdpMux for shared-port WebRTC-Direct inbound dispatch#1381
yashksaini-coder wants to merge 2 commits into
libp2p:mainfrom
yashksaini-coder:worktree-feat+webrtc-udpmux-1352

Conversation

@yashksaini-coder

@yashksaini-coder yashksaini-coder commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Fixes : #1352

Summary

  • Adds libp2p/transport/webrtc/_udp_mux.py — a UdpMux class (a shared asyncio.DatagramProtocol) that owns one OS UDP socket and demultiplexes concurrent inbound WebRTC-Direct dials
  • Pre-ICE: routes STUN BINDING REQUESTs by USERNAME ufrag prefix (parsed from the STUN message)
  • Post-ICE: routes DTLS/SCTP frames by remote (host, port) (registered after ICE nomination)
  • _MuxedTransport adapter gives each aioice.Connection a fake transport backed by the shared socket, bypassing _gather_candidates entirely so aioice never binds its own port
  • add_ice_connection(ufrag, pwd, host=) creates an aioice.Connection with preset local_username/local_password and injects a mux-backed StunProtocol into connection._protocols
  • 11 tests covering all dispatch paths, transport adapter, and connection factory

Why

Closes the ICE mux gap identified in the spike (#1352). A spec-aligned WebRTC-Direct listener (libp2p/specs#715) must advertise a single fixed UDP port and demux concurrent dials on that port — impossible with the current aioice architecture where each Connection binds its own ephemeral socket. UdpMux solves this without modifying aioice.

Architecture note

StunProtocol is an internal aioice class. We access it directly because we're injecting into aioice's _protocols list — this is intentional monkey-patching. The two # type: ignore annotations mark aioice's connection_lost(exc: Exception) annotation gap (should be Optional[Exception] per asyncio docs).

What's next

Follow-up PR: wire UdpMux into listener.py to replace the current HTTP-signaling harness with a spec-aligned STUN-dispatch listener (WebRTC-Direct v2, libp2p/specs#715).

Test plan

  • uv run pytest tests/core/transport/webrtc/test_udp_mux.py -v → 11/11 pass (skipped when aioice not installed)
  • uv run make lint → all pre-commit hooks pass (mypy + pyrefly clean)

…ch (libp2p#1352)

Implements `_udp_mux.UdpMux` — a shared UDP DatagramProtocol that owns a
single OS socket and demultiplexes concurrent inbound WebRTC-Direct dials:

- Pre-ICE STUN BINDING REQUESTs routed by `USERNAME` ufrag prefix
- Post-ICE DTLS/SCTP frames routed by remote (host, port)
- `_MuxedTransport` adapter gives each aioice.Connection a fake transport
  backed by the shared socket, bypassing `_gather_candidates` entirely
- `add_ice_connection()` creates an `aioice.Connection` with preset
  `local_username`/`local_password` and injects a mux-backed StunProtocol
  into its `_protocols` list without binding a new UDP socket

This is the foundational primitive for a spec-aligned WebRTC-Direct listener
(libp2p/specs#715) that advertises one fixed UDP port in its multiaddr.
Follow-up: wire UdpMux into listener.py for the full v2 listener flow.
- Remove Optional import, use X | None syntax (ruff pyupgrade)
- Fix import order in try block: from before import, alphabetical
- Add _udp_mux.py to pyrefly project_excludes (aioice not installed
  in lint tox env, same treatment as other webrtc-optional files)
- Drop unused _Connection import and unused `conn` variable in tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spike: aiortc ICE mux / STUN USERNAME exposure for webrtc-direct v2 listener

1 participant