From c58be3034dd8945d31f4d03730dfdf5986877096 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 26 Jul 2026 23:46:01 +0800 Subject: [PATCH] docs: webrtc 0.13 MSRV pin rationale and upgrade checklist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Cargo.toml | 6 +++++- src/webrtc.rs | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6010c0a84..c7b5b3d58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = [ diff --git a/src/webrtc.rs b/src/webrtc.rs index edcfea33d..7e0be3087 100644 --- a/src/webrtc.rs +++ b/src/webrtc.rs @@ -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` 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 {