Skip to content

feat(webrtc): run aiortc RTCPeerConnection over UdpMux (attach_muxed_connection) - #1448

Open
yashksaini-coder wants to merge 7 commits into
libp2p:mainfrom
yashksaini-coder:feat/webrtc-udpmux-aiortc-bridge
Open

feat(webrtc): run aiortc RTCPeerConnection over UdpMux (attach_muxed_connection)#1448
yashksaini-coder wants to merge 7 commits into
libp2p:mainfrom
yashksaini-coder:feat/webrtc-udpmux-aiortc-bridge

Conversation

@yashksaini-coder

Copy link
Copy Markdown
Contributor

What

Second step for #1437: lets a full aiortc RTCPeerConnection (DTLS + SCTP) run over a UdpMux-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 after createDataChannel, before any set*Description. Swaps iceGatherer._connection / iceTransport._connection for the mux-backed aioice.Connection and rebinds iceTransport._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.
  • UdpMux fixes surfaced by driving it with a real peer:
    • add_ice_connection also sets _local_candidates_start, so aiortc's gather() (issued from setLocalDescription) is a no-op instead of binding extra sockets and appending their candidates to the answer.
    • STUN responses carry no 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 ICE completed still routes.
    • unregister(ufrag) drops the learned addresses for that protocol.
    • STUN-shaped-but-malformed datagrams go straight to the connection's data path (StunProtocol.datagram_received only catches ValueError, so struct.error escaped the loop callback).
  • webrtc extra: 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

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

Copy link
Copy Markdown
Contributor Author

@seetadev @acul71 Rebased on latest main. This PR runs a full aiortc RTCPeerConnection over the #1397 UdpMux via a new attach_muxed_connection helper, plus mux hardening (address learning/caps, no extra sockets from gather()). Stacked on #1447; unblocks the STUN listener in #1449; refs #1437 / #1352. One known flake left: a Windows-only hang in the client-side aioice close (upstream proactor issue) we're isolating on a fork branch.

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.

1 participant