feat(webrtc): negotiate data channel message limits - #6560
Conversation
# Conflicts: # transports/webrtc-websys/src/stream/poll_data_channel.rs
|
Follow-up from browser-to-browser relay transfer testing: the negotiated With an 8 KiB negotiated frame limit, multiple individually valid This update adds 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:
This keeps the message-size API and the local resource-limit API separate, rather than making an application-layer workaround (such as skipping |
`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.
Description
Add a connection-level WebRTC data-channel message limit and negotiate an effective limit after the existing Noise authentication handshake.
Both native
libp2p-webrtcand browserlibp2p-webrtc-websysexposewith_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 remotemax-message-sizeSDP attribute and local sending capability, using the smaller applicable value. See WebRTC §6.1.1.2 and theRTCSctpTransport.maxMessageSizedefinition. 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
min(local, remote)as the connection's effective limit.StreamConfiginto 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 tokiocargo check -p libp2p-webrtc-websys --target wasm32-unknown-unknowngit diff --checkThe 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
noneif no AI was used): Codex 5.6Attestation (required):
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