use std::collections::HashMap; use std::io::{Error, ErrorKind}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex as StdMutex}; use std::time::Duration; use webrtc::api::setting_engine::SettingEngine; use webrtc::api::APIBuilder; use webrtc::data::data_channel::DataChannel as DetachedDataChannel; use webrtc::data_channel::RTCDataChannel; use webrtc::ice::mdns::MulticastDnsMode; use webrtc::ice_transport::ice_candidate::RTCIceCandidateInit; use webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType; use webrtc::ice_transport::ice_server::RTCIceServer; use webrtc::peer_connection::configuration::RTCConfiguration; use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; use webrtc::peer_connection::policy::ice_transport_policy::RTCIceTransportPolicy; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; use webrtc::peer_connection::RTCPeerConnection; use webrtc::stats::StatsReportType; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use base64::Engine; use bytes::{BufMut, Bytes, BytesMut}; use tokio::sync::{mpsc, watch, Mutex, Semaphore}; use tokio::time::{timeout, timeout_at, Instant}; use url::Url; use crate::bytes_codec::MAX_FRAME_LENGTH; use crate::config; use crate::protobuf::Message; use crate::sodiumoxide::crypto::secretbox::Key; use crate::ResultType; #[derive(Clone, Debug, PartialEq, Eq)] enum WebRTCConnectionState { Pending, Open, Closed(String), } pub struct WebRTCStream { pc: Arc, stream: Arc>>, state_notify: watch::Receiver, local_ice_rx: Arc>>>, session_key: String, send_timeout: u64, // Built with Relay-only ICE policy (force_relay): every selected pair goes through TURN. relay_only: bool, // Detached data channel, cached after the first `detach()` so send/recv do not re-lock and // re-fetch it per message. Shared across clones; `detach()` is idempotent. detached: Arc>>>, // Serialize a complete logical message across clones. Each fragment is a separate SCTP // message, so serializing only individual writes would allow two large messages to interleave. send_gate: Arc, // Receive-side reassembly state, guarded by a single mutex so the fragment accumulator // survives `next()` cancellation (e.g. `next_timeout`) instead of losing already-read // fragments mid-message. Assumes a single reader, consistent with the rest of the stream API. recv_state: Arc>, // True once the controller has completed the RustDesk identity binding (DTLS fingerprint // matched to the signed peer id, via `set_key`). DTLS always encrypts; this flag mirrors TCP's // "secured after key exchange" so key-less / unbound WebRTC is not shown as peer-authenticated. peer_verified: Arc, } #[derive(Default)] struct RecvState { // Accumulated payload of the logical message currently being reassembled. acc: BytesMut, // Reused read scratch buffer, avoiding a per-message allocation. scratch: Vec, } // The SCTP data channel's 65536-byte max message size is handled by // splitting a logical message into fragments carrying a 1-byte header. Fragment payload is kept // well under the limit so header+payload never reaches the exact-65536 boundary that the // receiver's reassembly would truncate with data loss. const MAX_FRAGMENT_PAYLOAD: usize = 60000; /// Receive scratch size: must be >= 1 (fragment header) + `MAX_FRAGMENT_PAYLOAD` and fit the /// negotiated SCTP max message size. const RECV_BUF_SIZE: usize = 64 * 1024; /// Fragment header byte: more fragments follow for this logical message. const FRAG_MORE: u8 = 1; /// Fragment header byte: final (or only) fragment of a logical message. const FRAG_END: u8 = 0; // use 3 public STUN servers to find out the NAT type, 2 must be the same address but different ports // https://stackoverflow.com/questions/72805316/determine-nat-mapping-behaviour-using-two-stun-servers // luckily nextcloud supports two ports for STUN // unluckily webrtc-rs does not use the same port to do the STUN request static DEFAULT_ICE_SERVERS: [&str; 3] = [ "stun:stun.cloudflare.com:3478", "stun:stun.nextcloud.com:3478", "stun:stun.nextcloud.com:443", ]; lazy_static::lazy_static! { static ref SESSIONS: Arc::>> = Default::default(); } impl Clone for WebRTCStream { fn clone(&self) -> Self { WebRTCStream { pc: self.pc.clone(), stream: self.stream.clone(), state_notify: self.state_notify.clone(), local_ice_rx: self.local_ice_rx.clone(), session_key: self.session_key.clone(), send_timeout: self.send_timeout, relay_only: self.relay_only, detached: self.detached.clone(), send_gate: self.send_gate.clone(), recv_state: self.recv_state.clone(), peer_verified: self.peer_verified.clone(), } } } impl WebRTCStream { #[inline] fn get_remote_offer(endpoint: &str) -> ResultType { // Ensure the endpoint starts with the "webrtc://" prefix if !endpoint.starts_with("webrtc://") { return Err( Error::new(ErrorKind::InvalidInput, "Invalid WebRTC endpoint format").into(), ); } // Extract the Base64-encoded SDP part let encoded_sdp = &endpoint["webrtc://".len()..]; // Decode the Base64 string let decoded_bytes = BASE64_STANDARD .decode(encoded_sdp) .map_err(|_| Error::new(ErrorKind::InvalidInput, "Failed to decode Base64 SDP"))?; Ok(String::from_utf8(decoded_bytes).map_err(|_| { Error::new( ErrorKind::InvalidInput, "Failed to convert decoded bytes to UTF-8", ) })?) } #[inline] fn sdp_to_endpoint(sdp: &str) -> String { let encoded_sdp = BASE64_STANDARD.encode(sdp); format!("webrtc://{}", encoded_sdp) } #[inline] fn get_key_for_sdp(sdp: &RTCSessionDescription) -> ResultType { let binding = sdp.unmarshal()?; let Some(fingerprint) = binding.attribute("fingerprint") else { // find fingerprint attribute in media descriptions for media in &binding.media_descriptions { if media.media_name.media != "application" { continue; } if let Some(fp) = media .attributes .iter() .find(|x| x.key == "fingerprint") .and_then(|x| x.value.clone()) { return Ok(fp); } } return Err(anyhow::anyhow!("SDP fingerprint attribute not found")); }; Ok(fingerprint.to_string()) } /// Process-local SESSIONS-map key: the DTLS fingerprint prefixed by role. An offerer and an /// answerer that share a fingerprint (a single process connecting to its own id) would /// otherwise collide, handing the offerer back as the answerer. The wire-level `session_key` /// used for ICE-candidate routing stays the bare fingerprint so both peers still match. #[inline] fn cache_key(fingerprint: &str, is_offerer: bool) -> String { format!( "{}:{}", if is_offerer { "offer" } else { "answer" }, fingerprint ) } #[inline] fn get_key_for_sdp_json(sdp_json: &str) -> ResultType { if sdp_json.is_empty() { return Ok("".to_string()); } let sdp = serde_json::from_str::(sdp_json)?; Self::get_key_for_sdp(&sdp) } #[inline] async fn get_key_for_peer(pc: &Arc, is_local: bool) -> ResultType { let Some(desc) = (match is_local { true => pc.local_description().await, false => pc.remote_description().await, }) else { return Err(anyhow::anyhow!("PeerConnection description is not set")); }; Self::get_key_for_sdp(&desc) } #[inline] fn get_ice_server_from_url(url: &str) -> Option { // standard url format with turn scheme: turn://user:pass@host:port match Url::parse(url) { Ok(u) => { if u.scheme() == "turn" || u.scheme() == "turns" || u.scheme() == "stun" || u.scheme() == "stuns" { Some(RTCIceServer { urls: vec![format!( "{}:{}:{}", u.scheme(), u.host_str().unwrap_or_default(), u.port().unwrap_or(3478) )], username: u.username().to_string(), credential: u.password().unwrap_or_default().to_string(), ..Default::default() }) } else { None } } Err(_) => None, } } /// Whether the ICE configuration contains a usable TURN server. A Relay-policy peer /// connection (force_relay) can only gather relay candidates, so without a TURN server it can /// never connect — callers use this to skip building a guaranteed-dead pc. pub fn has_turn_server() -> bool { Self::get_ice_servers().iter().any(|s| { s.urls .iter() .any(|u| u.starts_with("turn:") || u.starts_with("turns:")) }) } #[inline] fn get_ice_servers() -> Vec { let mut ice_servers = Vec::new(); let cfg = config::Config::get_option(config::keys::OPTION_ICE_SERVERS); let mut has_stun = false; for url in cfg.split(',').map(str::trim) { if let Some(ice_server) = Self::get_ice_server_from_url(url) { // Detect STUN in user config if ice_server .urls .iter() .any(|u| u.starts_with("stun:") || u.starts_with("stuns:")) { has_stun = true; } ice_servers.push(ice_server); } } // If there is no STUN (either TURN-only or empty config) → prepend defaults if !has_stun { ice_servers.insert( 0, RTCIceServer { urls: DEFAULT_ICE_SERVERS.iter().map(|s| s.to_string()).collect(), ..Default::default() }, ); } ice_servers } pub async fn new( remote_endpoint: &str, force_relay: bool, ms_timeout: u64, ) -> ResultType { // The endpoint contains a Base64-encoded SDP with host addresses and live ICE // credentials. Log only its size so debug logs cannot disclose that information. log::debug!( "New webrtc stream (remote endpoint: {} bytes)", remote_endpoint.len() ); let remote_offer = if remote_endpoint.is_empty() { "".into() } else { Self::get_remote_offer(remote_endpoint)? }; let mut key = Self::get_key_for_sdp_json(&remote_offer)?; let start_local_offer = remote_offer.is_empty(); if !key.is_empty() { let sessions_lock = SESSIONS.lock().await; if let Some(cached_stream) = sessions_lock.get(&Self::cache_key(&key, start_local_offer)) { log::debug!("Start webrtc with cached peer"); return Ok(cached_stream.clone()); } } // Create a SettingEngine and enable Detach let mut s = SettingEngine::default(); s.detach_data_channels(); s.set_ice_multicast_dns_mode(MulticastDnsMode::Disabled); // Create the API object let api = APIBuilder::new().with_setting_engine(s).build(); // Prepare the configuration, get ICE servers from config let config = RTCConfiguration { ice_servers: Self::get_ice_servers(), ice_transport_policy: if force_relay { RTCIceTransportPolicy::Relay } else { RTCIceTransportPolicy::All }, ..Default::default() }; let (notify_tx, notify_rx) = watch::channel(WebRTCConnectionState::Pending); let (ice_tx, ice_rx) = mpsc::unbounded_channel::(); // Create a new RTCPeerConnection let pc = Arc::new(api.new_peer_connection(config).await?); let local_ice_tx = ice_tx.clone(); pc.on_ice_candidate(Box::new(move |candidate| { let local_ice_tx = local_ice_tx.clone(); Box::pin(async move { let Some(candidate) = candidate else { return; }; match candidate.to_json() { Ok(candidate) => match serde_json::to_string(&candidate) { Ok(candidate_json) => { let _ = local_ice_tx.send(candidate_json); } Err(err) => { log::warn!("failed to serialize local ICE candidate: {}", err); } }, Err(err) => { log::warn!("failed to convert local ICE candidate to JSON: {}", err); } } }) })); let bootstrap_dc = if start_local_offer { let dc_open_notify = notify_tx.clone(); // Create a data channel with label "bootstrap" let dc = match pc.create_data_channel("bootstrap", None).await { Ok(dc) => dc, Err(e) => { // Close before propagating: the pc is live and would otherwise leak. pc.close().await.ok(); return Err(e.into()); } }; dc.on_open(Box::new(move || { log::debug!("Local data channel bootstrap open."); let _ = dc_open_notify.send(WebRTCConnectionState::Open); Box::pin(async {}) })); dc } else { // Wait for the data channel to be created by the remote peer // Here we create a dummy data channel to satisfy the type system Arc::new(RTCDataChannel::default()) }; let stream = Arc::new(Mutex::new(bootstrap_dc)); if !start_local_offer { // Register data channel creation handling let dc_open_notify = notify_tx.clone(); let stream_for_dc = stream.clone(); pc.on_data_channel(Box::new(move |dc: Arc| { let d_label = dc.label().to_owned(); let dc_open_notify2 = dc_open_notify.clone(); let stream_for_dc_clone = stream_for_dc.clone(); log::debug!("Remote data channel {} ready", d_label); Box::pin(async move { let mut stream_lock = stream_for_dc_clone.lock().await; *stream_lock = dc.clone(); drop(stream_lock); dc.on_open(Box::new(move || { let _ = dc_open_notify2.send(WebRTCConnectionState::Open); Box::pin(async {}) })); }) })); } // This will notify you when the peer has connected/disconnected let stream_for_close = stream.clone(); // Weak, not strong: a handler stored inside the pc that captured a strong // `Arc` forms a pc -> internal -> handler -> pc cycle that `close()` // never breaks (it only fires the handler) and no `Drop` clears, permanently leaking every // pc and the ICE-candidate sender's forwarding task. Upgrade inside the handler; if the pc // is already gone there is nothing left in SESSIONS to evict. let pc_for_close = Arc::downgrade(&pc); pc.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { let stream_for_close2 = stream_for_close.clone(); let on_connection_notify = notify_tx.clone(); let pc_for_close2 = pc_for_close.clone(); Box::pin(async move { log::debug!("WebRTC session peer connection state: {}", s); match s { // `Disconnected` is a transient, recoverable ICE state (webrtc-ice fires it // after ~5s without consent and returns to `Connected` when traffic resumes). // Only tear down on the terminal states so a short network blip (Wi-Fi roam, // sleep/wake, cell handover) does not permanently kill an established session. RTCPeerConnectionState::Failed | RTCPeerConnectionState::Closed => { let _ = on_connection_notify.send(WebRTCConnectionState::Closed( s.to_string(), )); log::debug!("WebRTC session closing due to {}", s); let _ = stream_for_close2.lock().await.close().await; log::debug!("WebRTC session stream closed"); let Some(pc_for_close2) = pc_for_close2.upgrade() else { return; }; let mut sessions_lock = SESSIONS.lock().await; match Self::get_key_for_peer(&pc_for_close2, start_local_offer).await { Ok(fingerprint) => { let k = Self::cache_key(&fingerprint, start_local_offer); // Only evict if the cached entry IS this pc: a duplicate offer // resolves to the same key, and closing the discarded duplicate pc // must not remove the live winner sharing that key. if sessions_lock .get(&k) .is_some_and(|s| Arc::ptr_eq(&s.pc, &pc_for_close2)) { sessions_lock.remove(&k); log::debug!("WebRTC session removed key: {}", k); } } Err(e) => { log::error!( "Failed to extract key for peer during session cleanup: {:?}", e ); // Fallback: try to remove any session associated with this peer connection let keys_to_remove: Vec = sessions_lock .iter() .filter_map(|(key, session)| { if Arc::ptr_eq(&session.pc, &pc_for_close2) { Some(key.clone()) } else { None } }) .collect(); for k in keys_to_remove { sessions_lock.remove(&k); log::debug!("WebRTC session removed by fallback key: {}", k); } } } } _ => {} } }) })); // process offer/answer // // Trickle ICE: the local description is returned WITHOUT waiting for candidate gathering // (candidates stream out via `take_local_ice_rx` afterwards), so this block is local-only // work — pc construction, DTLS cert keygen, SDP marshal — at sub-millisecond cost. The // controlled side awaits answer creation inline on its punch-reply critical path and // relies on that: adding any gathering/network wait here would delay the TCP/UDP // hole-punch reply for every connection. // Any failure below leaves a live pc with handlers already registered; its state handler // only fires on a terminal ICE state, so a bare `?`-drop would leak it (remotely // triggerable: a crafted `type:"answer"` offer passes the JSON+fingerprint pre-check but // fails `set_remote_description`). Close the pc before propagating any such error. let offer_answer: ResultType = async { if start_local_offer { let sdp = pc.create_offer(None).await?; pc.set_local_description(sdp.clone()).await?; // SDP carries host/srflx IPs and ICE ufrag/pwd; log only its size, not the body. log::debug!("local offer SDP built ({} bytes)", sdp.sdp.len()); let k = Self::get_key_for_sdp(&sdp)?; log::debug!("Start webrtc with local key: {}", k); Ok(k) } else { let sdp = serde_json::from_str::(&remote_offer)?; pc.set_remote_description(sdp.clone()).await?; let answer = pc.create_answer(None).await?; pc.set_local_description(answer).await?; log::debug!("remote offer SDP received ({} bytes)", sdp.sdp.len()); let k = Self::get_key_for_sdp(&sdp)?; log::debug!("Start webrtc with remote key: {}", k); Ok(k) } } .await; key = match offer_answer { Ok(k) => k, Err(e) => { pc.close().await.ok(); return Err(e); } }; let webrtc_stream = Self { pc, stream, state_notify: notify_rx, local_ice_rx: Arc::new(StdMutex::new(Some(ice_rx))), session_key: key.clone(), send_timeout: ms_timeout, relay_only: force_relay, detached: Arc::new(Mutex::new(None)), send_gate: Arc::new(Semaphore::new(1)), recv_state: Arc::new(Mutex::new(RecvState::default())), peer_verified: Arc::new(AtomicBool::new(false)), }; // Insert into the session cache, but never `await pc.close()` while holding this lock: // `close()` fires the peer-connection-state handler inline, which itself locks SESSIONS, // self-deadlocking the whole process. Resolve any duplicate off-lock. let cache_key = Self::cache_key(&key, start_local_offer); let duplicate = { let mut final_lock = SESSIONS.lock().await; if let Some(session) = final_lock.get(&cache_key) { Some(session.clone()) } else { final_lock.insert(cache_key, webrtc_stream.clone()); None } }; if let Some(session) = duplicate { // A concurrent `new()` already cached an equivalent stream; discard this pc's // resources (off-lock) and return the cached one. webrtc_stream.close().await; return Ok(session); } Ok(webrtc_stream) } #[inline] pub async fn get_local_endpoint(&self) -> ResultType { // Preserve the original one-shot endpoint contract: callers that only exchange this SDP // do not have a separate path for `take_local_ice_rx`, so their endpoint must contain the // gathered host/srflx/relay candidates. let mut gather_complete = self.pc.gathering_complete_promise().await; let _gathering_channel_closed = gather_complete.recv().await; self.get_local_endpoint_trickle().await } /// Return the current local description immediately for callers that signal candidates via /// `take_local_ice_rx`. Unlike `get_local_endpoint`, this does not wait for ICE gathering. #[inline] pub async fn get_local_endpoint_trickle(&self) -> ResultType { if let Some(local_desc) = self.pc.local_description().await { let sdp = serde_json::to_string(&local_desc)?; let endpoint = Self::sdp_to_endpoint(&sdp); Ok(endpoint) } else { Err(anyhow::anyhow!("Local desc is not set")) } } #[inline] pub async fn set_remote_endpoint(&self, endpoint: &str) -> ResultType<()> { let offer = Self::get_remote_offer(endpoint)?; log::debug!("WebRTC set remote sdp ({} bytes)", offer.len()); let sdp = serde_json::from_str::(&offer)?; self.pc.set_remote_description(sdp).await?; Ok(()) } /// DTLS certificate fingerprint of the local description (this endpoint's own cert). #[inline] pub async fn local_dtls_fingerprint(&self) -> ResultType { Self::get_key_for_peer(&self.pc, true).await } /// DTLS certificate fingerprint of the remote description (the peer's cert). webrtc-rs /// verifies the negotiated peer certificate against this fingerprint during the DTLS /// handshake, so once the channel is open a matching fingerprint identifies the peer's cert. #[inline] pub async fn remote_dtls_fingerprint(&self) -> ResultType { Self::get_key_for_peer(&self.pc, false).await } /// Whether the established connection runs through a TURN relay: `Some(true)` when the pc is /// Relay-policy (TURN is the only possibility) or the selected ICE candidate pair uses a /// relay candidate; `None` before a pair is selected. Feeds the UI's direct/relayed flag. pub async fn is_relayed(&self) -> Option { if self.relay_only { return Some(true); } let dtls = self.pc.sctp().transport(); dtls.ice_transport().get_selected_candidate_pair().await?; // webrtc 0.13 keeps RTCIceCandidatePair's candidates private. Its stats report exposes // the selected (nominated) pair and the corresponding candidate types instead. let stats = self.pc.get_stats().await; let pair = stats.reports.values().find_map(|report| match report { StatsReportType::CandidatePair(pair) if pair.nominated => Some(pair), _ => None, })?; let is_relay = |candidate_id: &str| { matches!( stats.reports.get(candidate_id), Some( StatsReportType::LocalCandidate(candidate) | StatsReportType::RemoteCandidate(candidate) ) if RTCIceCandidateType::from(candidate.candidate_type) == RTCIceCandidateType::Relay ) }; Some( is_relay(&pair.local_candidate_id) || is_relay(&pair.remote_candidate_id), ) } #[inline] pub fn take_local_ice_rx(&self) -> Option> { self.local_ice_rx.lock().ok().and_then(|mut rx| rx.take()) } #[inline] pub async fn add_remote_ice_candidate(&self, candidate_json: &str) -> ResultType<()> { if candidate_json.is_empty() { return Ok(()); } let candidate = serde_json::from_str::(candidate_json)?; self.pc.add_ice_candidate(candidate).await?; Ok(()) } #[inline] pub fn session_key(&self) -> &str { &self.session_key } pub async fn wait_connected(&mut self, ms: u64) -> ResultType<()> { if ms > 0 { match timeout(Duration::from_millis(ms), self.wait_for_connect_result()).await { Ok(result) => result?, Err(_) => return Err(anyhow::anyhow!("WebRTC wait_connected timeout")), } } else { self.wait_for_connect_result().await?; } Ok(()) } /// Explicitly tear down the peer connection. /// /// Dropping a `WebRTCStream` handle is not enough to release the underlying /// `RTCPeerConnection`: the global `SESSIONS` map holds a clone, so the pc (and its /// ICE/DTLS/STUN resources) would stay alive until it happens to reach a terminal ICE /// state. Closing here fires `on_peer_connection_state_change`, which removes the /// `SESSIONS` entry, so callers that abandon a stream (e.g. a raced offerer that lost to /// another transport) should call this to avoid leaking it. #[inline] pub async fn close(&self) { self.pc.close().await.ok(); } #[inline] pub fn set_raw(&mut self) { // not-supported } #[inline] pub fn local_addr(&self) -> SocketAddr { SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0) } #[inline] pub fn set_send_timeout(&mut self, ms: u64) { self.send_timeout = ms; } #[inline] pub fn set_key(&mut self, _key: Key) { // WebRTC traffic is DTLS-encrypted regardless; the secretbox key is unused. // Callers invoke set_key only after the controller has bound the DTLS fingerprint to the // verified peer identity (or the controlled side has completed the matching handshake). // Mark peer-verified so is_secured() matches TCP's post-key-exchange meaning. self.peer_verified.store(true, Ordering::Release); } #[inline] pub fn is_secured(&self) -> bool { self.peer_verified.load(Ordering::Acquire) } #[inline] pub async fn send(&mut self, msg: &impl Message) -> ResultType<()> { self.send_raw(msg.write_to_bytes()?).await } #[inline] pub async fn send_raw(&mut self, msg: Vec) -> ResultType<()> { self.send_bytes(Bytes::from(msg)).await } #[inline] async fn wait_for_connect_result(&mut self) -> ResultType<()> { loop { match self.state_notify.borrow().clone() { WebRTCConnectionState::Open => return Ok(()), WebRTCConnectionState::Closed(reason) => { return Err(anyhow::anyhow!("WebRTC connection closed: {}", reason)); } WebRTCConnectionState::Pending => {} } self.state_notify.changed().await?; } } /// Fetch (and cache) the detached data channel. `detach()` is idempotent and returns a /// clone of the same underlying channel, so caching it just avoids re-locking per message. async fn detached_dc(&self) -> ResultType> { { let cache = self.detached.lock().await; if let Some(dc) = cache.as_ref() { return Ok(dc.clone()); } } let raw = self.stream.lock().await.clone(); let dc = raw.detach().await?; let mut cache = self.detached.lock().await; // Another task may have cached it while we were detaching. if let Some(existing) = cache.as_ref() { return Ok(existing.clone()); } *cache = Some(dc.clone()); Ok(dc) } 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. 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 { Ok(Ok(permit)) => permit, Ok(Err(err)) => { return Err(Error::new( ErrorKind::BrokenPipe, format!("WebRTC send gate closed: {}", err), ) .into()); } Err(_) => { if let Err(err) = self.pc.close().await { log::warn!("failed to close WebRTC after send timeout: {}", err); } return Err(Error::new(ErrorKind::TimedOut, "WebRTC send timeout").into()); } }; match timeout_at(deadline, self.send_bytes_inner(bytes)).await { Ok(res) => res, Err(_) => { // Keep the logical-message permit while closing so no waiting clone can append // a new message after a partially-written fragment sequence. if let Err(err) = self.pc.close().await { log::warn!("failed to close WebRTC after send timeout: {}", err); } Err(Error::new(ErrorKind::TimedOut, "WebRTC send timeout").into()) } } } else { let _send_permit = send_gate.acquire_owned().await.map_err(|err| { Error::new( ErrorKind::BrokenPipe, format!("WebRTC send gate closed: {}", err), ) })?; self.send_bytes_inner(bytes).await } } async fn send_bytes_inner(&mut self, bytes: Bytes) -> ResultType<()> { if bytes.len() > MAX_FRAME_LENGTH { return Err(Error::new(ErrorKind::InvalidInput, "Overflow").into()); } self.wait_for_connect_result().await?; let dc = self.detached_dc().await?; let data = bytes.as_ref(); let mut offset = 0; // Always emit at least one fragment (a lone FRAG_END header for an empty message), so a // zero-length data-channel message — which the receiver cannot distinguish from EOF — is // never sent. loop { let end = (offset + MAX_FRAGMENT_PAYLOAD).min(data.len()); let is_last = end >= data.len(); let chunk = &data[offset..end]; let mut framed = BytesMut::with_capacity(1 + chunk.len()); framed.put_u8(if is_last { FRAG_END } else { FRAG_MORE }); framed.put_slice(chunk); dc.write(&framed.freeze()).await?; offset = end; if is_last { break; } } Ok(()) } #[inline] pub async fn next(&mut self) -> Option> { if let Err(err) = self.wait_for_connect_result().await { self.pc.close().await.ok(); return Some(Err(Error::new(ErrorKind::Other, err.to_string()))); } let dc = match self.detached_dc().await { Ok(dc) => dc, Err(err) => { self.pc.close().await.ok(); return Some(Err(Error::new(ErrorKind::Other, err.to_string()))); } }; // Hold recv_state across the reassembly loop: the accumulator must survive `next()` // cancellation (e.g. next_timeout) so already-read fragments are not lost mid-message. let mut st = self.recv_state.lock().await; if st.scratch.len() < RECV_BUF_SIZE { st.scratch.resize(RECV_BUF_SIZE, 0); } loop { let RecvState { acc, scratch } = &mut *st; let n = match dc.read(scratch.as_mut_slice()).await { Ok(n) => n, Err(err) => { self.pc.close().await.ok(); return Some(Err(Error::new( ErrorKind::Other, format!("data channel read error: {}", err), ))); } }; if n == 0 { // Clean EOF: the remote reset the stream or shut its write half. An empty logical // message is represented by a 1-byte header, so it is never confused with EOF. self.pc.close().await.ok(); return None; } acc.extend_from_slice(&scratch[1..n]); // Match TCP's maximum frame size while preventing an unbounded FRAG_MORE stream from // exhausting memory. if acc.len() > MAX_FRAME_LENGTH { acc.clear(); self.pc.close().await.ok(); return Some(Err(Error::new( ErrorKind::Other, "WebRTC reassembled message exceeded maximum frame size", ))); } if scratch[0] == FRAG_END { let msg = std::mem::take(acc); return Some(Ok(msg)); } } } #[inline] pub async fn next_timeout(&mut self, ms: u64) -> Option> { match timeout(Duration::from_millis(ms), self.next()).await { Ok(res) => res, Err(_) => None, } } } pub fn is_webrtc_endpoint(endpoint: &str) -> bool { // use sdp base64 json string as endpoint, or prefix webrtc: endpoint.starts_with("webrtc://") } #[cfg(test)] mod tests { use crate::config; use crate::webrtc::WebRTCStream; use crate::webrtc::DEFAULT_ICE_SERVERS; use std::{sync::Arc, time::Duration}; use tokio::sync::Barrier; use tokio::time::timeout; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; #[test] fn test_webrtc_ice_url() { assert_eq!( WebRTCStream::get_ice_server_from_url("turn://example.com:3478") .unwrap_or_default() .urls[0], "turn:example.com:3478" ); assert_eq!( WebRTCStream::get_ice_server_from_url("turn://example.com") .unwrap_or_default() .urls[0], "turn:example.com:3478" ); assert_eq!( WebRTCStream::get_ice_server_from_url("turn://123@example.com") .unwrap_or_default() .username, "123" ); assert_eq!( WebRTCStream::get_ice_server_from_url("turn://123@example.com") .unwrap_or_default() .credential, "" ); assert_eq!( WebRTCStream::get_ice_server_from_url("turn://123:321@example.com") .unwrap_or_default() .credential, "321" ); assert_eq!( WebRTCStream::get_ice_server_from_url("stun://example.com:3478") .unwrap_or_default() .urls[0], "stun:example.com:3478" ); assert_eq!( WebRTCStream::get_ice_server_from_url("http://123:123@example.com:3478"), None ); config::Config::set_option("ice-servers".to_string(), "".to_string()); assert_eq!( WebRTCStream::get_ice_servers()[0].urls[0], DEFAULT_ICE_SERVERS[0].to_string() ); config::Config::set_option( "ice-servers".to_string(), ",stun://example.com,turn://example.com,sdf".to_string(), ); assert_eq!( WebRTCStream::get_ice_servers()[0].urls[0], "stun:example.com:3478" ); assert_eq!( WebRTCStream::get_ice_servers()[1].urls[0], "turn:example.com:3478" ); assert_eq!(WebRTCStream::get_ice_servers().len(), 2); config::Config::set_option( "ice-servers".to_string(), "".to_string(), ); } #[test] fn test_webrtc_session_key() { let mut sdp_str = "".to_owned(); assert_eq!( WebRTCStream::get_key_for_sdp( &RTCSessionDescription::offer(sdp_str).unwrap_or_default() ) .unwrap_or_default(), "" ); sdp_str = "\ v=0 o=- 7400546379179479477 208696200 IN IP4 0.0.0.0 s=- t=0 0 a=fingerprint:sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88 a=group:BUNDLE 0 a=extmap-allow-mixed m=application 9 UDP/DTLS/SCTP webrtc-datachannel c=IN IP4 0.0.0.0 a=setup:actpass a=mid:0 a=sendrecv a=sctp-port:5000 a=ice-ufrag:RMWjjpXfpXbDPdMz a=ice-pwd:BtIqlWHfwhsJdFiBROeLuEbNmYfHxRfT".to_owned(); assert_eq!( WebRTCStream::get_key_for_sdp( &RTCSessionDescription::offer(sdp_str).unwrap_or_default() ).unwrap_or_default(), "sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88" ); sdp_str = "\ v=0 o=- 7400546379179479477 208696200 IN IP4 0.0.0.0 s=- t=0 0 a=group:BUNDLE 0 a=extmap-allow-mixed m=application 9 UDP/DTLS/SCTP webrtc-datachannel c=IN IP4 0.0.0.0 a=fingerprint:sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88 a=setup:actpass a=mid:0 a=sendrecv a=sctp-port:5000 a=ice-ufrag:RMWjjpXfpXbDPdMz a=ice-pwd:BtIqlWHfwhsJdFiBROeLuEbNmYfHxRfT".to_owned(); assert_eq!( WebRTCStream::get_key_for_sdp( &RTCSessionDescription::offer(sdp_str).unwrap_or_default() ).unwrap_or_default(), "sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88" ); sdp_str = "\ v=0 o=- 7400546379179479477 208696200 IN IP4 0.0.0.0 s=- t=0 0 a=group:BUNDLE 0 a=extmap-allow-mixed m=application 9 UDP/DTLS/SCTP webrtc-datachannel c=IN IP4 0.0.0.0 a=setup:actpass a=mid:0 a=sendrecv a=sctp-port:5000 a=ice-ufrag:RMWjjpXfpXbDPdMz a=ice-pwd:BtIqlWHfwhsJdFiBROeLuEbNmYfHxRfT" .to_owned(); assert!( WebRTCStream::get_key_for_sdp( &RTCSessionDescription::offer(sdp_str).unwrap_or_default() ) .is_err(), "can not find fingerprint attribute" ); sdp_str = "\ v=0 o=- 7400546379179479477 208696200 IN IP4 0.0.0.0 s=- t=0 0 a=group:BUNDLE 0 a=extmap-allow-mixed m=audio 9 UDP/DTLS/SCTP webrtc-datachannel c=IN IP4 0.0.0.0 a=fingerprint:sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88 a=setup:actpass a=mid:0 a=sendrecv a=sctp-port:5000 a=ice-ufrag:RMWjjpXfpXbDPdMz a=ice-pwd:BtIqlWHfwhsJdFiBROeLuEbNmYfHxRfT".to_owned(); assert!( WebRTCStream::get_key_for_sdp( &RTCSessionDescription::offer(sdp_str).unwrap_or_default() ) .is_err(), "can not find datachannel fingerprint attribute" ); assert!( WebRTCStream::get_key_for_sdp( &RTCSessionDescription::offer("".to_owned()).unwrap_or_default() ) .is_err(), "invalid sdp should error" ); assert!( WebRTCStream::get_key_for_sdp_json("{}").is_err(), "empty sdp json should error" ); assert!( WebRTCStream::get_key_for_sdp_json("{ss}").is_err(), "invalid sdp json should error" ); let endpoint = "webrtc://eyJ0eXBlIjoiYW5zd2VyIiwic2RwIjoidj0wXHJcbm89LSA0MTA1NDk3NTY2NDgyMTQzODEwIDYwMzk1NzQw\ MCBJTiBJUDQgMC4wLjAuMFxyXG5zPS1cclxudD0wIDBcclxuYT1maW5nZXJwcmludDpzaGEtMjU2IDYxOjYwOjc0OjQwOjI4OkNFOjBCOjBDOjc1OjRCOj\ EwOjlBOkVFOjc3OkY1OjQ0OjU3Ojg0OjUxOkRCOjA0OjkyOjRBOjEwOjFDOjRFOjVGOjdFOkYxOkIzOjcxOjIyXHJcbmE9Z3JvdXA6QlVORExFIDBcclxu\ YT1leHRtYXAtYWxsb3ctbWl4ZWRcclxubT1hcHBsaWNhdGlvbiA5IFVEUC9EVExTL1NDVFAgd2VicnRjLWRhdGFjaGFubmVsXHJcbmM9SU4gSVA0IDAuMC\ 4wLjBcclxuYT1zZXR1cDphY3RpdmVcclxuYT1taWQ6MFxyXG5hPXNlbmRyZWN2XHJcbmE9c2N0cC1wb3J0OjUwMDBcclxuYT1pY2UtdWZyYWc6SHlnU1Rr\ V2RsRlpHRG1XWlxyXG5hPWljZS1wd2Q6SkJneFZWaGZveVhHdHZha1VWcnBQeHVOSVpMU3llS1pcclxuYT1jYW5kaWRhdGU6OTYzOTg4MzQ4IDEgdWRwID\ IxMzA3MDY0MzEgMTkyLjE2OC4xLjIgNjQwMDcgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6OTYzOTg4MzQ4IDIgdWRwIDIxMzA3MDY0MzEgMTkyLjE2OC4x\ LjIgNjQwMDcgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6MTg2MTA0NTE5MCAxIHVkcCAxNjk0NDk4ODE1IDE0LjIxMi42OC4xMiAyNzAwNCB0eXAgc3JmbH\ ggcmFkZHIgMC4wLjAuMCBycG9ydCA2NDAwOFxyXG5hPWNhbmRpZGF0ZToxODYxMDQ1MTkwIDIgdWRwIDE2OTQ0OTg4MTUgMTQuMjEyLjY4LjEyIDI3MDA0\ IHR5cCBzcmZseCByYWRkciAwLjAuMC4wIHJwb3J0IDY0MDA4XHJcbmE9ZW5kLW9mLWNhbmRpZGF0ZXNcclxuIn0=".to_owned(); assert_eq!( WebRTCStream::get_key_for_sdp_json( &WebRTCStream::get_remote_offer(&endpoint).unwrap_or_default() ).unwrap_or_default(), "sha-256 61:60:74:40:28:CE:0B:0C:75:4B:10:9A:EE:77:F5:44:57:84:51:DB:04:92:4A:10:1C:4E:5F:7E:F1:B3:71:22" ); } #[tokio::test] async fn test_webrtc_new_stream() { let mut endpoint = "webrtc://sdfsdf".to_owned(); assert!( WebRTCStream::new(&endpoint, false, 10000).await.is_err(), "invalid webrtc endpoint should error" ); endpoint = "wss://sdfsdf".to_owned(); assert!( WebRTCStream::new(&endpoint, false, 10000).await.is_err(), "invalid webrtc endpoint should error" ); assert!( WebRTCStream::new("", false, 10000).await.is_ok(), "local webrtc endpoint should ok" ); endpoint = "webrtc://eyJ0eXBlIjoiYW5zd2VyIiwic2RwIjoidj0wXHJcbm89LSA0MTA1NDk3NTY2NDgyMTQzODEwIDYwMzk1NzQw\ MCBJTiBJUDQgMC4wLjAuMFxyXG5zPS1cclxudD0wIDBcclxuYT1maW5nZXJwcmludDpzaGEtMjU2IDYxOjYwOjc0OjQwOjI4OkNFOjBCOjBDOjc1OjRCOj\ EwOjlBOkVFOjc3OkY1OjQ0OjU3Ojg0OjUxOkRCOjA0OjkyOjRBOjEwOjFDOjRFOjVGOjdFOkYxOkIzOjcxOjIyXHJcbmE9Z3JvdXA6QlVORExFIDBcclxu\ YT1leHRtYXAtYWxsb3ctbWl4ZWRcclxubT1hcHBsaWNhdGlvbiA5IFVEUC9EVExTL1NDVFAgd2VicnRjLWRhdGFjaGFubmVsXHJcbmM9SU4gSVA0IDAuMC\ 4wLjBcclxuYT1zZXR1cDphY3RpdmVcclxuYT1taWQ6MFxyXG5hPXNlbmRyZWN2XHJcbmE9c2N0cC1wb3J0OjUwMDBcclxuYT1pY2UtdWZyYWc6SHlnU1Rr\ V2RsRlpHRG1XWlxyXG5hPWljZS1wd2Q6SkJneFZWaGZveVhHdHZha1VWcnBQeHVOSVpMU3llS1pcclxuYT1jYW5kaWRhdGU6OTYzOTg4MzQ4IDEgdWRwID\ IxMzA3MDY0MzEgMTkyLjE2OC4xLjIgNjQwMDcgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6OTYzOTg4MzQ4IDIgdWRwIDIxMzA3MDY0MzEgMTkyLjE2OC4x\ LjIgNjQwMDcgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6MTg2MTA0NTE5MCAxIHVkcCAxNjk0NDk4ODE1IDE0LjIxMi42OC4xMiAyNzAwNCB0eXAgc3JmbH\ ggcmFkZHIgMC4wLjAuMCBycG9ydCA2NDAwOFxyXG5hPWNhbmRpZGF0ZToxODYxMDQ1MTkwIDIgdWRwIDE2OTQ0OTg4MTUgMTQuMjEyLjY4LjEyIDI3MDA0\ IHR5cCBzcmZseCByYWRkciAwLjAuMC4wIHJwb3J0IDY0MDA4XHJcbmE9ZW5kLW9mLWNhbmRpZGF0ZXNcclxuIn0=".to_owned(); assert!( WebRTCStream::new(&endpoint, false, 10000).await.is_err(), "connect to an 'answer' webrtc endpoint should error" ); } #[tokio::test] async fn test_webrtc_wait_connected_timeout() { let mut stream = WebRTCStream::new("", false, 100).await.unwrap(); let err = stream.wait_connected(10).await.unwrap_err(); assert!(err.to_string().contains("timeout")); } async fn connect_loopback() -> (WebRTCStream, WebRTCStream) { let mut offerer = WebRTCStream::new("", false, 20000).await.unwrap(); let offer = offerer.get_local_endpoint_trickle().await.unwrap(); let answerer = WebRTCStream::new(&offer, false, 20000).await.unwrap(); let answer = answerer.get_local_endpoint_trickle().await.unwrap(); offerer.set_remote_endpoint(&answer).await.unwrap(); // Bridge trickle candidates directly between the two peers, both directions. let mut off_ice = offerer.take_local_ice_rx().unwrap(); let mut ans_ice = answerer.take_local_ice_rx().unwrap(); let answerer_for_ice = answerer.clone(); let offerer_for_ice = offerer.clone(); tokio::spawn(async move { while let Some(c) = off_ice.recv().await { let _ = answerer_for_ice.add_remote_ice_candidate(&c).await; } }); tokio::spawn(async move { while let Some(c) = ans_ice.recv().await { let _ = offerer_for_ice.add_remote_ice_candidate(&c).await; } }); offerer.wait_connected(20000).await.unwrap(); let mut answerer = answerer; answerer.wait_connected(20000).await.unwrap(); (offerer, answerer) } // One-shot callers exchange only the endpoints and never consume `take_local_ice_rx`. #[tokio::test] async fn test_webrtc_loopback_gathered_endpoints() { let connect = async { let mut offerer = WebRTCStream::new("", false, 20000).await.unwrap(); let offer = offerer.get_local_endpoint().await.unwrap(); let mut answerer = WebRTCStream::new(&offer, false, 20000).await.unwrap(); let answer = answerer.get_local_endpoint().await.unwrap(); offerer.set_remote_endpoint(&answer).await.unwrap(); offerer.wait_connected(20000).await.unwrap(); answerer.wait_connected(20000).await.unwrap(); offerer.close().await; answerer.close().await; }; timeout(Duration::from_secs(40), connect) .await .expect("gathered-endpoint WebRTC loopback did not complete in time"); } // In-process offerer<->answerer loopback exercising the send/next data plane that the framing, // empty-message, and EOF fixes live in. Connects over host candidates (works offline; any // configured/default STUN just fails in the background without blocking the host pair). #[tokio::test] async fn test_webrtc_loopback_roundtrip() { let connect = async { let (mut offerer, mut answerer) = connect_loopback().await; // Host-candidate loopback is direct, never TURN-relayed. assert_eq!(offerer.is_relayed().await, Some(false)); // Small message. offerer.send_raw(b"hello".to_vec()).await.unwrap(); let got = answerer.next().await.unwrap().unwrap(); assert_eq!(&got[..], b"hello"); // Empty message: must round-trip as an empty frame, not be seen as EOF. offerer.send_raw(Vec::new()).await.unwrap(); let got = answerer.next().await.unwrap().unwrap(); assert_eq!(got.len(), 0, "empty message must not be treated as EOF"); // Payload far above the 64KB single-message cap: must be fragmented and reassembled. let big = vec![0xABu8; 200_000]; offerer.send_raw(big.clone()).await.unwrap(); let got = answerer.next().await.unwrap().unwrap(); assert_eq!(got.len(), big.len(), "large message must survive fragmentation"); assert_eq!(&got[..], &big[..]); // Reverse direction. answerer.send_raw(b"world".to_vec()).await.unwrap(); let got = offerer.next().await.unwrap().unwrap(); assert_eq!(&got[..], b"world"); // Peer close: the other side observes a clean EOF (None) or a close error, never a hang. offerer.close().await; match timeout(Duration::from_secs(10), answerer.next()).await { Ok(None) | Ok(Some(Err(_))) => {} Ok(Some(Ok(b))) => panic!("expected EOF after peer close, got {} bytes", b.len()), Err(_) => panic!("answerer.next() hung after peer close"), } answerer.close().await; }; timeout(Duration::from_secs(40), connect) .await .expect("webrtc loopback did not complete in time"); } #[tokio::test] async fn test_webrtc_concurrent_large_sends_preserve_boundaries() { let connect = async { let (offerer, mut answerer) = connect_loopback().await; let mut sender_a = offerer.clone(); let mut sender_b = offerer.clone(); let expected_a = vec![0xAA; 200_000]; let expected_b = vec![0xBB; 200_000]; let payload_a = expected_a.clone(); let payload_b = expected_b.clone(); let barrier = Arc::new(Barrier::new(3)); let barrier_a = barrier.clone(); let send_a = tokio::spawn(async move { barrier_a.wait().await; sender_a.send_raw(payload_a).await }); let barrier_b = barrier.clone(); let send_b = tokio::spawn(async move { barrier_b.wait().await; sender_b.send_raw(payload_b).await }); barrier.wait().await; let receive = async { let first = answerer.next().await.unwrap().unwrap(); let second = answerer.next().await.unwrap().unwrap(); (first, second) }; let (send_a, send_b, (first, second)) = tokio::join!(send_a, send_b, receive); send_a.unwrap().unwrap(); send_b.unwrap().unwrap(); let boundaries_preserved = (first.as_ref() == expected_a.as_slice() && second.as_ref() == expected_b.as_slice()) || (first.as_ref() == expected_b.as_slice() && second.as_ref() == expected_a.as_slice()); assert!(boundaries_preserved, "concurrent messages were interleaved"); offerer.close().await; answerer.close().await; }; timeout(Duration::from_secs(40), connect) .await .expect("concurrent WebRTC sends did not complete in time"); } }