Skip to content

feat(webrtc): negotiate data channel message limits - #6560

Open
yexiyue wants to merge 5 commits into
libp2p:masterfrom
yexiyue:feat/webrtc-message-limits
Open

feat(webrtc): negotiate data channel message limits#6560
yexiyue wants to merge 5 commits into
libp2p:masterfrom
yexiyue:feat/webrtc-message-limits

Conversation

@yexiyue

@yexiyue yexiyue commented Jul 24, 2026

Copy link
Copy Markdown

Description

Add a connection-level WebRTC data-channel message limit and negotiate an effective limit after the existing Noise authentication handshake.

Both native libp2p-webrtc and browser libp2p-webrtc-websys expose with_max_message_size. The selected effective value is used consistently by the libp2p frame codec, write high-water mark, browser buffered-amount accounting, and the native WebRTC read buffer.

Fixes #6557.

Why

The WebRTC DataChannel stack has a message-size limit below the libp2p stream abstraction. The current fixed 16 KiB libp2p frame limit follows RFC 8831's guidance for SCTP without message interleaving, but deployments can have a smaller effective limit. In a browser-to-browser relay path, this led to an underlying SCTP implementation rejecting a frame and closing the data channel.

W3C WebRTC specifies that the maximum size accepted by RTCDataChannel.send() is derived from the remote max-message-size SDP attribute and local sending capability, using the smaller applicable value. See WebRTC §6.1.1.2 and the RTCSctpTransport.maxMessageSize definition. RFC 8831 also recommends a 16 KiB maximum when SCTP message interleaving is unavailable: RFC 8831 §6.7.

WebRTC Direct does not exchange arbitrary application SDP between libp2p peers: each endpoint synthesizes the remote SDP needed to establish the direct connection. Consequently, putting a local configuration value in that SDP would not negotiate the remote capability and could make the two framing layers disagree.

Design

  1. The Noise handshake data channel continues to use the historical 16 KiB compatible limit.
  2. Once both peers are authenticated, they exchange a fixed-width advertised limit on that protected channel.
  3. New peers use min(local, remote) as the connection's effective limit.
  4. A peer that closes the channel immediately after Noise is treated as a legacy peer and falls back to the historical 16 KiB limit.
  5. SDP remains protocol-fixed; the negotiated value is only carried by StreamConfig into the framing and transport implementations.

This avoids a global conservative limit while preserving compatibility with existing WebRTC Direct peers. The capability value is exchanged only after Noise has authenticated the peer and bound the DTLS fingerprints into its prologue.

Validation

  • cargo test -p libp2p-webrtc-utils — 26 tests, including smaller-limit selection and legacy/invalid-peer fallback.
  • cargo test -p libp2p-webrtc --features tokio --test smoke smoke — real two-node UDP WebRTC Direct connection.
  • cargo check -p libp2p-webrtc --features tokio
  • cargo check -p libp2p-webrtc-websys --target wasm32-unknown-unknown
  • git diff --check

The existing multi-node WebRTC smoke test was also run. It completes successfully, but its detached background tasks log cleanup-time NoListeners / SendError(Disconnected) panics, so I do not present it as a clean regression signal in this PR.

AI Assistance Disclosure

Tools used (required — write none if no AI was used): Codex 5.6

Attestation (required):

  • I have read every line of this diff, understand what it does, and can explain it in review.

Notes & open questions

The post-Noise exchange deliberately uses the existing reserved handshake data channel rather than introducing a new SDP extension or a second negotiated data channel. It adds no new public wire format to application substreams.

Change checklist

  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • A changelog entry has been made in the appropriate crates

@yexiyue
yexiyue marked this pull request as ready for review July 24, 2026 15:44
yexiyue added a commit to yexiyue/rust-libp2p that referenced this pull request Jul 25, 2026
# Conflicts:
#	transports/webrtc-websys/src/stream/poll_data_channel.rs
@yexiyue

yexiyue commented Jul 25, 2026

Copy link
Copy Markdown
Author

Follow-up from browser-to-browser relay transfer testing: the negotiated max_message_size must not also cap the browser callback's aggregate receive queue.

With an 8 KiB negotiated frame limit, multiple individually valid RTCDataChannel.onmessage events can be delivered before the deferred Rust waker is polled. Using that 8 KiB value as the aggregate read_buffer limit incorrectly reports Remote is overloading us with messages and resets the stream, even though no individual message exceeds the negotiated limit.

This update adds webrtc_websys::Config::with_max_read_buffer_size(NonZeroUsize). It is deliberately local to the browser transport and is not negotiated. The default is 256 KiB; the effective value is max(configured_read_buffer_size, negotiated_message_size), so one valid message always fits while the queue remains bounded.

I added focused tests for consecutive valid 8 KiB messages, the configured bound, and a negotiated message larger than the default. Validation also passed with:

  • cargo test -p libp2p-webrtc-websys
  • cargo check -p libp2p-webrtc-websys --target wasm32-unknown-unknown
  • browser-to-browser WebRTC relay transfer in SwarmDrop after rebuilding the wasm package

This keeps the message-size API and the local resource-limit API separate, rather than making an application-layer workaround (such as skipping flush) responsible for a transport-level queueing policy.

`render_description` took a `{max_message_size}` placeholder but the context still
filled in `16 * 1024`, so `with_max_message_size` reached the framing layer while
SDP kept announcing 16 KiB regardless.

That only stays harmless while the configured limit is *below* 16 KiB, as it is by
default: the endpoint then sends less than it advertised. Configure anything larger
and the peer's SCTP is told to expect 16 KiB while messages up to the new limit
arrive.

Both `sdp::answer` and `sdp::render_description` now take the `StreamConfig` the
framing layer is built from, so the advertised limit and the enforced one cannot
drift apart. Both call sites already had it in scope.

The test asserts four different sizes: a single one would also pass against a
hard-coded value that happens to match it, which is how this survived review.
The high-water mark is a *lower* bound on when to flush, not an upper bound on the
buffer: `poll_ready` flushes while `buffer.len() >= hwm`, then `start_send` appends a
whole frame. Setting it to `max_data_size()` therefore allowed a short frame — one that
leaves the buffer below the mark — to be written out together with the full-size frame
that followed it.

The layer below turns one write into exactly one SCTP user message, so that coalesced
write becomes a message larger than the negotiated `max_message_size`. webrtc-rs rejects
it with "outbound packet larger than maximum message size" and the frame is simply lost;
the byte stream above never re-syncs.

Measured on a 1 MiB transfer with an 8 KiB limit: 125 writes of 8190 B and three of
8419 B. SCTP rejected exactly those three, and the receiver ended up 49,467 B short.

This was previously masked: the SDP always advertised a hard-coded 16 KiB while this
repo's framing used 8 KiB, so the oversized writes still fit under the advertised limit.
Advertising the configured size honestly (previous commit) exposed it, and configuring
anything above 8 KiB exposed it even before that.

The regression test mixes frame sizes on purpose — a run of full-size frames never
reproduces it, because each one lands the buffer above the mark and gets flushed on its
own, leaving nothing to coalesce with.
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.

WebRTC relay path can reject the default 16 KiB framed message size

1 participant