Skip to content

fix(webrtc): decode data-channel framing as a byte stream; SCTP stream-id parity by DTLS role - #1460

Open
yashksaini-coder wants to merge 6 commits into
libp2p:mainfrom
yashksaini-coder:fix/webrtc-go-interop-framing
Open

fix(webrtc): decode data-channel framing as a byte stream; SCTP stream-id parity by DTLS role#1460
yashksaini-coder wants to merge 6 commits into
libp2p:mainfrom
yashksaini-coder:fix/webrtc-go-interop-framing

Conversation

@yashksaini-coder

@yashksaini-coder yashksaini-coder commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

What

Two pre-existing bugs that made every py↔go WebRTC-Direct connection fail, found by running the v1 code live against go-libp2p v0.49.0:

  • Framing: we required one complete uvarint + pb.Message per SCTP message, but go-msgio writes the varint prefix and the body as two separate writes → two SCTP messages. py failed at Noise msg#1 with malformed handshake frame length (and WebRTCStream.on_data would have dropped every split frame after that). Now both the Noise channel and streams decode frames from an accumulating byte buffer: split or batched frames are fine, the buffer is bounded by MAX_MESSAGE_SIZE, frames decoded before a malformed one are still delivered, a malformed frame resets the stream (RESET sent to the peer, further bytes ignored), and a whole SCTP message is applied in one trio hop.
  • SCTP stream-id parity: aiortc seeds its DCEP id allocator from the ICE role, which is inverted for a WebRTC-Direct listener (ICE-controlled but DTLS server), so our listener's ids collided with go's even dialer ids (assert stream_id not in self._data_channels). We now re-seed the allocator's parity from the DTLS role (RFC 8832: client even, server odd) and let aiortc keep allocating and recycling ids.

Also widens one Windows-sensitive loopback test's budget (two dials + two bounded closes).

Verified

Live against go-libp2p v0.49.0, both directions: go→py and py→go connect and complete Noise. Unit tests for split/batched frames, partial-delivery-then-reset, id parity and id reuse. tests/core/transport/webrtc: 230 passed; mypy/ruff/pyrefly clean.

Refs #1437 — first of two stacked PRs; the v2 flow (#1459) builds on this.

…rity by DTLS role

go-libp2p (go-msgio pbio fallback) writes the uvarint prefix and the
protobuf body as two SCTP messages, so every py<->go WebRTC-Direct
connection died at Noise msg#1 with "malformed handshake frame length".
New stream._decode_frames pops complete frames off an accumulating
buffer (frames may be split or batched across SCTP messages, bounded by
MAX_MESSAGE_SIZE); DataChannelReadWriter and WebRTCStream.on_data both
use it. A malformed frame now resets the stream instead of dropping one
message and desynchronising.

With the handshake passing, a py listener then hit aiortc's
`assert stream_id not in self._data_channels`: aiortc picks DCEP stream
id parity from the ICE role, which is inverted for a WebRTC-Direct
listener (ICE-controlled, DTLS server), so it used even ids like go's
dialer. _create_channel now passes an explicit id by DTLS role
(RFC 8832: client even, server odd).

Verified live against go-libp2p v0.49.0: go->py and py->go, v1 and v2.

Refs libp2p#1437
yashksaini-coder and others added 5 commits August 28, 2026 12:47
- keep aiortc allocating (and recycling) SCTP stream ids; only re-seed the
  parity from the DTLS role (explicit ids tied the 16-bit space to our
  never-recycled counter)
- on_data: deliver frames decoded before a malformed one, reset the peer
  (RESET + cleanup) instead of a local-only reset, ignore bytes after reset,
  and apply a whole SCTP message in one trio hop
- _decode_frames: derive the max prefix length from MAX_MESSAGE_SIZE

Refs libp2p#1437
The test performs two full dials and two bounded peer-connection closes;
on Windows each close can take ~5 s when a datagram write is in flight, so
30 s was flaky (seen once on CI, no socket errors logged).

Refs libp2p#1437
… collides

Harness mode binds TCP on the same port number as the OS-chosen UDP mux
port; Windows reserves port ranges per protocol, so that TCP bind can fail
with WinError 10013 (seen on CI in the harness loopback test). Retry up to
five times with a new UDP port before giving up; explicit ports still fail
loudly.

Refs libp2p#1437
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.

2 participants