feat(webrtc): run aiortc RTCPeerConnection over UdpMux (attach_muxed_connection) - #1448
Open
yashksaini-coder wants to merge 7 commits into
Open
feat(webrtc): run aiortc RTCPeerConnection over UdpMux (attach_muxed_connection)#1448yashksaini-coder wants to merge 7 commits into
yashksaini-coder wants to merge 7 commits into
Conversation
yashksaini-coder
force-pushed
the
feat/webrtc-udpmux-aiortc-bridge
branch
2 times, most recently
from
August 16, 2026 14:00
b7c8870 to
9e1fd41
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
…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
yashksaini-coder
force-pushed
the
feat/webrtc-udpmux-aiortc-bridge
branch
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 |
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