feat(webrtc): run aiortc RTCPeerConnection over UdpMux (attach_muxed_connection) - #1448
Merged
acul71 merged 11 commits intoAug 28, 2026
Merged
Conversation
yashksaini-coder
force-pushed
the
feat/webrtc-udpmux-aiortc-bridge
branch
3 times, most recently
from
August 19, 2026 17:57
7279f50 to
bc0a1b5
Compare
This was referenced Aug 19, 2026
Contributor
Author
|
@seetadev @acul71 Rebased on latest main. This PR runs a full aiortc |
yashksaini-coder
added a commit
to yashksaini-coder/py-libp2p
that referenced
this pull request
Aug 23, 2026
…_attr Dialer v1 ICE-credential fields on the aioice Connection and the listener's inbound _validate_peer_identity replacement now assert the slot exists before writing, matching the pattern introduced in libp2p#1448. Refs libp2p#1437
yashksaini-coder
force-pushed
the
feat/webrtc-udpmux-aiortc-bridge
branch
from
August 27, 2026 04:35
07ab523 to
8596cdf
Compare
yashksaini-coder
added a commit
to yashksaini-coder/py-libp2p
that referenced
this pull request
Aug 27, 2026
…_attr Dialer v1 ICE-credential fields on the aioice Connection and the listener's inbound _validate_peer_identity replacement now assert the slot exists before writing, matching the pattern introduced in libp2p#1448. Refs libp2p#1437
yashksaini-coder
added a commit
to yashksaini-coder/py-libp2p
that referenced
this pull request
Aug 27, 2026
…_attr Dialer v1 ICE-credential fields on the aioice Connection and the listener's inbound _validate_peer_identity replacement now assert the slot exists before writing, matching the pattern introduced in libp2p#1448. Refs libp2p#1437
yashksaini-coder
force-pushed
the
feat/webrtc-udpmux-aiortc-bridge
branch
from
August 27, 2026 11:43
7963c3e to
f2f656e
Compare
tox commands_pre installed no extras, so aiortc was absent in CI and every test under tests/core/transport/webrtc was skipped on GitHub. aiortc 1.15 is pure Python and its native deps (av, pylibsrtp) ship manylinux/win wheels, so no system packages are needed. Refs libp2p#1437
…oles The listener's inbound completion was a stub: after ICE it logged and returned, so no inbound connection ever reached the handler. dial() was broken too - get_remote_fingerprint read attributes aiortc does not have. - listener: own a trio nursery (system task, like TCP), hop asyncio->trio without blocking the loop, run Noise XX as *initiator* (spec: server initiates, dialer responds), then hand the authenticated connection to the handler; bound in-flight unauthenticated inbounds. - transport.dial(): Noise responder; verify authenticated peer ID against /p2p/ after the handshake. - noise: role-ordered prologue (dialer fingerprint, then server); PatternXX.handshake_outbound(remote_peer=None) skips only the ID equality check; DataChannelReadWriter.read(n) honours n (the Noise packet reader asks for the 2-byte prefix alone). - helpers: get_remote_fingerprint reads pc.sctp.transport._ssl peer cert; noise send waits for data channel 0 to open. - tests: transport-level dial->listen->stream echo loopback, wrong /p2p/ rejection, e2e Noise role test, buffered read test, fingerprint test. Refs libp2p#1437
- Frame Noise handshake bytes as uvarint-prefixed webrtc.pb.Message stream frames on channel 0 (spec 'Multiplexing'; what go/js do) instead of raw bytes - required for interop; py<->py was symmetric so tests did not notice. - Listener: guard the handler call - the nursery lives in a trio system task, so an escaping exception aborted the whole trio run. - dial(): bound the Noise phase (responder's first step is a read) with handshake_timeout; close the PC on every failure path, not just the two explicit mismatches; do not pass config.ice_servers (previously no STUN servers were used; the default Google STUN added ~5s to offline dials). - Tests: framing round-trip/chunking/FIN/malformed, handler-exception regression. Refs libp2p#1437
…string Review follow-up: the module docstring still described the prologue as local_fp || remote_fp; it is dialer_fp || server_fp (spec role order), matching build_noise_prologue. Also bound the dialer's wait_for_connected with config.handshake_timeout like the listener does. Refs libp2p#1437
…connection) aiortc has no injection point for an external ICE connection, so a listener that demuxes on one UDP port could not use RTCPeerConnection on top of UdpMux. Add attach_muxed_connection(pc, mux, conn): swaps the gatherer/transport aioice.Connection for the mux-backed one, rebinds the DTLS _recv/_send that RTCIceTransport captured at construction, and keeps the mux tables in sync on ICE state changes. UdpMux fixes found while validating that path with a real peer: - mark _local_candidates_start so aiortc's gather() from setLocalDescription is a no-op (it bound extra sockets and appended their candidates to the answer); - STUN responses/indications carry no USERNAME - route them by address, and learn peer addresses from inbound checks and outbound sends; - unregister(ufrag) also drops the addresses learned for that protocol; - STUN-shaped-but-malformed datagrams go straight to the connection's data path (StunProtocol only catches ValueError, so struct.error escaped). Test: mux-backed server PC vs plain aiortc client PC on loopback - answer advertises only the shared port, ICE/DTLS/SCTP connect, data flows both ways, tables empty after close. webrtc extra now aiortc>=1.15. Refs libp2p#1437
… mux test - _learn_addr: latest connection wins for a reused (ip, port) (pion behaviour) so a redial from the same socket reaches its new connection; cap learned addresses per protocol (unauthenticated STUN with a live ufrag from many source ports must not grow the table without bound). - test_pc_over_mux: bind the mux on 0.0.0.0 and advertise a real host address - a 127.0.0.1-bound socket cannot reply to a LAN-bound peer socket on Windows (WinError 1231), which Linux's weak-host model hid. Refs libp2p#1437
A hung close() after a primary failure would outlive asyncio.wait_for and be killed by pytest-timeout, reported as an xdist 'worker crashed' that hides the real error (seen on Windows CI). Refs libp2p#1437
…ection) pc.close() deadlocks on Windows CPython 3.12/3.13: DTLS queues its close_notify datagram, transport.close() runs with that write in flight and defers connection_lost to the write callback, but _ProactorDatagramTransport._loop_writing early-returns on _conn_lost - the deferred connection_lost is never delivered and aioice's StunProtocol.close() awaits its closed-future forever (confirmed via task/transport dumps on CI: closing=True, write_fut finished, closed.done=False). close_peer_connection(pc) bounds close() and, on timeout, abort()s the lingering aioice transports - _force_close delivers connection_lost unconditionally - then lets close() finish. Used in the PC-over-mux test; production call sites switch in the listener PR. Refs libp2p#1437
Writing a wrong - or upgraded-away - private/mangled attribute name is a silent no-op: setattr creates a new attribute the library never reads and the failure only surfaces far downstream (the SDP-fingerprint invariant test, an ICE that binds extra sockets). Assert hasattr at the write so it fails at the line that caused it, and so an aiortc/aioice upgrade that renames or drops a slot breaks loudly at construction. Applied to the DTLS cert pin (_RTCPeerConnection__certificates, ca8331f), the attach_muxed_connection injection points, and add_ice_connection's aioice fields. Refs libp2p#1437
…Windows Windows surfaces an ICMP port-unreachable for a datagram we sent (a keepalive to a peer that just closed) as WSAECONNRESET on the shared socket's next recv, and the proactor loop does not re-arm reading after error_received - the mux went deaf for every other peer (seen in CI as '[WinError 10054]' followed by the next dial's ICE timing out). Disable that behaviour with SIO_UDP_CONNRESET on the mux socket; no-op elsewhere. Refs libp2p#1437
yashksaini-coder
force-pushed
the
feat/webrtc-udpmux-aiortc-bridge
branch
from
August 27, 2026 11:48
f2f656e to
59369a8
Compare
yashksaini-coder
added a commit
to yashksaini-coder/py-libp2p
that referenced
this pull request
Aug 27, 2026
…_attr Dialer v1 ICE-credential fields on the aioice Connection and the listener's inbound _validate_peer_identity replacement now assert the slot exists before writing, matching the pattern introduced in libp2p#1448. Refs libp2p#1437
acul71
added a commit
that referenced
this pull request
Aug 28, 2026
…1449) Lands the full WebRTC-Direct stack for #1437: - CI webrtc extra, inbound wiring + spec Noise roles, UdpMux bridge - STUN-dispatch v1 listener + v1 dialer (spec path default) - HTTP POST /sdp harness opt-in via enable_sdp_http_harness - Review follow-ups: rate limit, ice_servers=[], actpass + DTLS server role, 0.0.0.0 concrete advertising, README note Supersedes #1446, #1447, #1448. Refs #1437
24 of 30 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Second step for #1437: lets a full aiortc
RTCPeerConnection(DTLS + SCTP) run over aUdpMux-backed ICE connection, so a listener can demux concurrent inbound dials on one UDP port.attach_muxed_connection(pc, mux, conn)(_aiortc_helpers.py): call aftercreateDataChannel, before anyset*Description. SwapsiceGatherer._connection/iceTransport._connectionfor the mux-backedaioice.Connectionand rebindsiceTransport._recv/_send(aiortc captures those at construction — without this DTLS keeps talking to the orphaned connection). Registers/unregisters the peer address on ICE state changes.UdpMuxfixes surfaced by driving it with a real peer:add_ice_connectionalso sets_local_candidates_start, so aiortc'sgather()(issued fromsetLocalDescription) is a no-op instead of binding extra sockets and appending their candidates to the answer.USERNAME— they were dropped. Now routed by address; addresses are learned from inbound checks and from outbound sends (pion-style), so DTLS racing ahead of our ICEcompletedstill routes.unregister(ufrag)drops the learned addresses for that protocol.StunProtocol.datagram_receivedonly catchesValueError, sostruct.errorescaped the loop callback).webrtcextra:aiortc>=1.15,<2.0(what this was validated against; 1.15 also fixed the credential-forwarding bug from spike: aiortc ICE mux / STUN USERNAME exposure for webrtc-direct v2 listener #1352).Tests
test_udp_mux.py: mux-backed server PC vs plain aiortc client PC on loopback — answer advertises only the shared port + our creds +a=setup:passive, ICE/DTLS/SCTP connect, data flows both ways, mux tables empty after close. Plus addr-learning / unregister / malformed-STUN cases.tests/core/transport/webrtc: 187 passed; mypy/pyrefly clean.Stacked on #1447 → #1446.
Refs #1437