Skip to content

sctp: don't discard received-but-undelivered data on incoming stream reset - #1

Open
lann wants to merge 43 commits into
masterfrom
sctp-reset-undelivered-data
Open

sctp: don't discard received-but-undelivered data on incoming stream reset#1
lann wants to merge 43 commits into
masterfrom
sctp-reset-undelivered-data

Conversation

@lann

@lann lann commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Problem

When a peer sends N messages on a stream and then immediately resets it (the RFC 8831 §6.7 close-after-send pattern), the tail DATA chunk(s) and the outgoing-stream-reset RECONFIG are often processed by the receiver in the same input batch. reset_streams_if_any called unregister_stream outright, removing the stream and its reassembly queue with received-but-unread messages still inside. The application intermittently saw the channel close with only a prefix of the messages delivered (receive-side silent data loss, timing-dependent).

Per RFC 6525, data received before the reset must still be delivered to the upper layer.

Full analysis: SCTP-RESET-DISCARDS-UNDELIVERED-DATA.md (observed ~1/3 failure rate on the channel-close-flush conformance test against a libwebrtc sender). Confirmed still present at upstream webrtc-rs/rtc HEAD (a10cd2c).

Fix

In reset_streams_if_any, when the in-order condition holds but the stream's reassembly queue is still readable:

  • mark the stream reset_pending, keep it registered in read-only state (RecvSendState::Readable), and queue a StreamEvent::Readable so the consumer drains it;
  • refuse new inbound DATA for reset-pending streams in handle_data;
  • complete unregister_stream (emitting AssociationLost { reason: Reset }) from read_sctp once the queue is drained.

The reset is still answered "Success - Performed" as before — only local delivery is deferred.

Testing

New regression test test_assoc_reset_defers_teardown_until_undelivered_data_read reproduces the exact failure deterministically (4 writes + stop() in one batch; pre-fix it fails with ErrStreamNotExisted before reading message 0) and verifies all messages are delivered, the stream is torn down after draining, and AssociationLost(Reset) is emitted.

  • cargo test -p rtc-sctp: 118 passed
  • cargo clippy -p rtc-sctp --all-targets: no new warnings

2026-08-06: rebased onto upstream webrtc-rs/rtc master (afcc19f, post-0.21 crypto migration); still applies cleanly — upstream v0.20.0 and master both retain the unconditional unregister_stream on incoming reset. cargo test -p rtc-sctp (121 tests + the new regression test), fmt, and clippy pass on the rebased branch.

mirsella and others added 2 commits July 23, 2026 16:52
…address (webrtc-rs#136)

Connectivity checks and data writes for a server-reflexive local
candidate were tagged with the candidate's NAT-mapped address, which no
local socket is bound to; drivers routing outbound transmits by
transport.local_addr had to drop them, so ICE never connected when the
srflx path was the only viable one.

Per RFC 8445 sec 6.1.2, checks for a reflexive candidate must be sent
from its base. Add Candidate::base_addr() (related address for
srflx/prflx, addr() otherwise) and use it in Agent::send_stun and the
peer connection ICE handler write path.
yexiyue and others added 27 commits July 28, 2026 17:16
…s#140)

`RTCDataChannelInit` derived `Default`, so `ordered` came out `false` — contradicting
both the field's own doc comment ("The default value of `true` guarantees that data
will be delivered in order") and the W3C dictionary, where `ordered` is defined as
`= true`.

`create_data_channel(label, None)` did not consult that default at all: it left
`DataChannelParameters` on its own derived default, which is `false` too. Route `None`
through `RTCDataChannelInit::default()` so the documented defaults have a single
definition.

An unordered channel is not merely out-of-order. Unordered chunks bypass SCTP's
ordered-delivery queue, so a first message can overtake the `DATA_CHANNEL_OPEN` sent on
the same stream; the peer receives user data on a stream it has not accepted yet,
`RTCDataChannelInternal::accept` rejects it as a non-DCEP PPID, and that error is
logged and discarded on the pipeline's read pass. The message is lost with no error
reaching either side.

Closes webrtc-rs#139
…tc-rs#138)

`RTCDataChannel::send` checked only that the channel was registered, which it is
from `create_data_channel` onwards. The condition that actually matters --
whether its SCTP stream exists -- was checked later, in
`DataChannelHandler::handle_write`, and that runs on the pipeline's write pass
where an `Err` is logged and discarded:

    if let Err(err) = handler.handle_write(msg) {
        warn!("{}.handle_write got error: {}", handler.name(), err);
    }

So the caller was handed `Ok(())` for a message that was dropped on the floor,
with only a stray warning to show for it. Sending on an already closed channel
had the same shape.

Check the real condition at the send boundary instead, where the error can
still reach the caller: `ErrDataChannelNotOpen` (new variant -- `Error` is
`#[non_exhaustive]`) while the channel is `connecting`, `ErrDataChannelClosed`
once it is gone. Keeping them distinct matters: the first is worth retrying
after the channel opens, the second never is.

This also stops a rejected send from charging `outstanding_bytes`. Those bytes
never entered the SCTP pipeline, so nothing would ever release them, and the
leaked counter would permanently shrink the channel's send window.

Note this errors where W3C `send()` prescribes buffering for a `connecting`
channel. Buffering is the better contract and a much larger change; erroring is
the smallest step that stops the silent data loss, and it is what the write path
already decided -- it just could not say so.
…-rs#137)

`SettingEngine::disable_certificate_fingerprint_verification` had a field and
a setter but no reader: the flag was never passed to `RTCDtlsTransport`, so the
handshake always installed the fingerprint-matching `verify_peer_certificate`
callback and enabling the option had no observable effect.

The neighbouring `allow_insecure_verification_algorithm` is plumbed through the
exact same path, which makes the omission easy to spot side by side.

Pass the flag down and build the callback only when the comparison is wanted.
`with_verify_peer_certificate` takes an `Option`, and leaving it out is what
disables the check — `insecure_skip_verify` is already true, so this callback is
the only thing standing between the peer's certificate and acceptance. It does
not weaken the "a certificate must be presented" requirement: `client_auth` is
`RequireAnyClientCert`, which the DTLS layer enforces on its own (flight4
rejects an empty `peer_certificates` with `ErrClientCertificateRequired` before
the callback would run).

This is required by protocols where the answerer cannot know the offerer's
fingerprint ahead of time. libp2p's WebRTC-Direct is the canonical case: the
server synthesizes the client's offer locally with a placeholder fingerprint and
authenticates the peer afterwards with a Noise handshake over the data channel.
Without this, the DTLS handshake fails with ErrNoMatchingCertificateFingerprint.

Adds an integration test covering both directions — a mismatched fingerprint
connects with the option enabled, and still fails with it left at the default.
… the peer connection can have one concrete type
…tcp-processing-boxed to demonstrate how to use RTCPeerConnection<BoxedInterceptor> directly
…default set offers ECDHE_RSA suites incompatible with an ECDSA certificate (handshake stalls)
…alidate` (webrtc-rs#141)

* Allow RSA key for DTLS

* Cleanup

* Move related peer connection functionality out of existing DTLS test

* Add integration test for RSA dtls keys

* Prevent unused warning
rainliu and others added 14 commits August 2, 2026 22:46
[Pre-1.0] G3 — Crypto provider unification   webrtc-rs#128
P8 — Integrate with the async webrtc repository
… MessageIntegrity<'a> (webrtc-rs#145)

* remove-public-API-crypto_provider-from-RTCPeerConnection

* refactor MessageIntegrity<'a>

* fix compiler errors

* refactor rtc-srtp

* fix cargo build --workspace --all-targets --features crypto-ring,crypto-aws-lc-rs
…reset

When a peer sends messages and then immediately resets the stream
(close-after-send), the DATA chunks and the outgoing-stream-reset
RECONFIG can be processed in the same input batch. reset_streams_if_any
would unregister the stream outright, dropping the reassembly queue
with received-but-unread messages still inside, so the application saw
the channel close with only a prefix of the messages delivered.

Per RFC 6525, data received before the reset must still be delivered to
the upper layer. Defer the stream teardown when the reassembly queue is
still readable: keep the stream registered in read-only state (marked
reset_pending), refuse new inbound DATA for it, and complete
unregister_stream (emitting AssociationLost/Reset) once the consumer
drains the queue via read_sctp. The reset is still answered
"Success - Performed" as before.
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.

5 participants