Skip to content

Negotiated (Option<u16>) data channels may hit ErrStreamAlreadyExist or drop inbound when peer sends before local creation #748

@kaito-harry

Description

@kaito-harry

Summary

When both peers use negotiated (Option=Some(id), out-of-band) RTCDataChannels with the same id, if the remote peer sends user data before the local negotiated channel is created, the local side may hit ErrStreamAlreadyExist or fail to receive messages.

Observed behavior

  • Remote sends DATA on stream id (no DCEP in negotiated mode). Receiver creates/attaches a stream for that id. When the local app later dials the same id, AssociationInternal::open_stream returns ErrStreamAlreadyExist because the stream already exists.
  • Some paths expect DCEP only for non‑negotiated channels. If the negotiated channel isn’t created locally yet, the first inbound user‑data frame (non‑DCEP PPID) can be rejected by the server path.

Code references

  • Duplicate id check (returns ErrStreamAlreadyExist): sctp/src/association/association_internal.rs (open_stream)
  • Dial path that always opens the stream: data/src/data_channel/mod.rs (DataChannel::dial)
  • Server path that expects DCEP for non‑negotiated: data/src/data_channel/mod.rs (server path check for DCEP PPID)
  • ID allocation context (non‑negotiated, even/odd by DTLS role): webrtc/src/sctp_transport/mod.rs (generate_and_set_data_channel_id)

Expected behavior

  • For negotiated channels (Option), allow attaching an inbound stream by known id without DCEP, even if the local app hasn’t created the channel yet; or
  • On local open collision, gracefully attach to the existing inbound stream instead of returning ErrStreamAlreadyExist, so the application always gets a single consistent RTCDataChannel object.

Proposed improvements

  1. In negotiated mode, match inbound stream id to known negotiated channels and bind directly (bypass DCEP requirement).
  2. On open_stream ErrStreamAlreadyExist for a negotiated id, bind the negotiated channel to the existing inbound stream and return it to the caller.
  3. Optionally buffer inbound data on that stream until the negotiated channel is created.

Reproduction

  1. Both sides create channels with negotiated=Some(42).
  2. Answerer sends data immediately after SCTP is established.
  3. Offerer creates the negotiated channel later and tries to dial id=42.
  4. Offerer sees ErrStreamAlreadyExist or “no messages” on the locally created object, while data actually flows to the inbound-created object.

Why this matters

Negotiated mode avoids DCEP by design per spec, but real apps can send early. Improving robustness makes negotiated channels less timing‑fragile without changing non‑negotiated semantics.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions