docs: webrtc 0.13 MSRV pin rationale and upgrade checklist

- Cargo.toml: record why webrtc is pinned to 0.13 — >=0.14 pulls sdp 0.10 /
  webrtc-util 0.12 using usize::is_multiple_of (needs rustc >=1.87), while
  rustdesk CI builds with Rust 1.75 (sciter i128 ABI pin)
- module-level upgrade checklist in src/webrtc.rs listing the version-coupled
  webrtc-rs internals this transport relies on (SCTP write backpressure,
  64KB message cap, detach() semantics, handler-capture leak cycle,
  Disconnected transience, stats-based is_relayed), all verified against
  webrtc 0.13 / webrtc-data 0.11 / webrtc-sctp 0.12
- send_bytes: document the bounded-backpressure mechanism (128 KiB PendingQueue
  semaphore + cwnd/rwnd cap) and that it is NOT cancel-safe

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rustdesk
2026-07-26 23:46:01 +08:00
parent e0307245dc
commit c58be3034d
2 changed files with 42 additions and 1 deletions
+5 -1
View File
@@ -65,6 +65,10 @@ rustls-pki-types = "1.11"
rustls-native-certs = "0.8"
webpki-roots = "1.0.4"
async-recursion = "1.1"
# Pinned to 0.13: webrtc >=0.14 pulls sdp 0.10 / webrtc-util 0.12, which use
# usize::is_multiple_of (needs rustc >=1.87), while rustdesk CI builds with Rust 1.75
# (sciter i128 ABI pin, flutter-build.yml). Bump only after CI's Rust moves past 1.87,
# and work through the upgrade checklist at the top of src/webrtc.rs first.
webrtc = { version = "0.13.0", optional = true }
libloading = "0.8"
@@ -78,7 +82,7 @@ protobuf-codegen = { version = "3.7" }
[dev-dependencies]
clap = "4.5.51"
webrtc = "0.13.0"
webrtc = "0.13.0" # keep in lockstep with [dependencies] webrtc (rustc 1.75 pin, see above)
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = [
+37
View File
@@ -1,3 +1,32 @@
//! WebRTC transport for RustDesk streams.
//!
//! # webrtc crate upgrade checklist
//!
//! The webrtc crate version is MSRV-pinned in Cargo.toml (see the comment there). Beyond plain
//! API compatibility, this module relies on webrtc-rs *internals* that its public API does not
//! guarantee. All of them were verified against webrtc 0.13 (webrtc-data 0.11, webrtc-sctp 0.12);
//! re-verify each against the new crate sources when bumping:
//!
//! - **Send backpressure is bounded**: `data::DataChannel::write` PARKS when webrtc-sctp's
//! PendingQueue is full (byte-counting semaphore, `QUEUE_BYTES_LIMIT` = 128 KiB; permits return
//! as chunks drain) and inflight data is cwnd/rwnd-capped (peer default rwnd 1 MiB).
//! `send_bytes` depends on this both for bounded memory on slow links and for its
//! send_timeout-then-close semantics. If a new version buffers unboundedly instead, video can
//! OOM a slow session and the send timeout never fires.
//! - **Max SCTP message size 65536**: `MAX_FRAGMENT_PAYLOAD` + 1 header byte must stay below it.
//! - **`detach()` is an idempotent Arc clone with no close-on-drop** (`detached_dc` caches it and
//! clones are shared across `WebRTCStream` clones).
//! - **`on_*` handlers are stored inside the pc**: a handler capturing a strong
//! `Arc<RTCPeerConnection>` forms an uncollectable cycle and leaks the pc permanently — see the
//! `Arc::downgrade` in `new()`; any newly added handler must follow it.
//! - **`Disconnected` peer-connection state is transient/recoverable** (ICE consent lapse);
//! only `Failed`/`Closed` are treated as terminal by the state handler.
//! - **Stats-based `is_relayed()`**: `RTCIceCandidatePair`'s candidates are private in 0.13;
//! 0.17+ makes them `pub`, allowing direct field access instead of the stats scan.
//!
//! Then re-run the loopback tests at the bottom of this file (`cargo test --features webrtc
//! webrtc::tests`).
use std::collections::HashMap;
use std::io::{Error, ErrorKind};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
@@ -742,12 +771,20 @@ impl WebRTCStream {
Ok(dc)
}
/// NOT cancel-safe: dropping this future mid-message (e.g. wrapping it in `select!`/`timeout`)
/// can leave a partial fragment sequence on the wire, corrupting reassembly of every later
/// message on this stream. A caller that abandons a send must treat the stream as dead and
/// close it; the built-in `send_timeout` path below already does (it closes the pc).
pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> {
let send_timeout = self.send_timeout;
let send_gate = self.send_gate.clone();
// Bound the WHOLE data-channel send (wait-for-open + every write) by send_timeout,
// including time queued behind another clone. Without this a write can park indefinitely
// on SCTP pending-queue backpressure and connection.rs's timeout timer never runs.
// That parking is also what bounds sender memory: webrtc-sctp's PendingQueue admits at
// most 128 KiB (byte-counting semaphore) and inflight data is cwnd/rwnd-capped, so a slow
// link parks the write here until this timeout closes the pc — TCP-send-timeout
// equivalent. Verified against webrtc-sctp 0.12; see the module-level upgrade checklist.
if send_timeout > 0 {
let deadline = Instant::now() + Duration::from_millis(send_timeout);
let _send_permit = match timeout_at(deadline, send_gate.acquire_owned()).await {