mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-11 15:01:02 +03:00
webrtc: skip the controller's ICE re-send instead of queueing it twice
The controller sends every candidate twice, because the server's hop to a peer registered over UDP can lose one. The ICE agent that dedups repeats sits downstream of the answerer's queue, so the answerer paid for both copies: a slot, a JSON parse, and the ICE agent's lock, once per repeat. Remember a digest of what was queued and skip the repeat. Recorded only once queued, so a candidate a full queue refused stays repairable by the re-send. The queue's depth is unchanged. A real peer gathers well under it - four STUN servers, link-local IPv6 filtered, one component - and the drain empties it as candidates trickle in, so what this removes is the redundant work, not an overflow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{hash_map::RandomState, HashMap, VecDeque},
|
||||||
|
hash::BuildHasher,
|
||||||
net::SocketAddr,
|
net::SocketAddr,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
@@ -53,12 +54,16 @@ lazy_static::lazy_static! {
|
|||||||
static ref SOLVING_PK_MISMATCH: Mutex<String> = Default::default();
|
static ref SOLVING_PK_MISMATCH: Mutex<String> = Default::default();
|
||||||
static ref LAST_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now()));
|
static ref LAST_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now()));
|
||||||
static ref LAST_RELAY_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now()));
|
static ref LAST_RELAY_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now()));
|
||||||
static ref WEBRTC_ICE_TXS: Mutex<HashMap<String, mpsc::Sender<String>>> = Default::default();
|
static ref WEBRTC_ICE_TXS: Mutex<HashMap<String, IceRoute>> = Default::default();
|
||||||
|
static ref ICE_DIGEST_STATE: RandomState = Default::default();
|
||||||
}
|
}
|
||||||
/// Remote ICE candidates buffered per session while the answerer applies them. Mirrors the
|
/// Remote ICE candidates buffered per session while the answerer applies them. Same depth as the
|
||||||
/// controller's own cap: gathering yields host, then srflx, then relay, so a real peer sends
|
/// controller's own buffer (`Client::MAX_PENDING_WEBRTC_ICE`), though that one evicts its oldest
|
||||||
/// well under this, and anything past it is someone deciding how much memory this process holds.
|
/// where a full channel here refuses the newest.
|
||||||
const MAX_PENDING_REMOTE_ICE: usize = 64;
|
const MAX_PENDING_REMOTE_ICE: usize = 64;
|
||||||
|
/// Queued candidates remembered so the controller's re-send is skipped instead of taking a slot
|
||||||
|
/// of its own. Far more than an honest peer gathers, at eight bytes each.
|
||||||
|
const ICE_DEDUP_WINDOW: usize = 256;
|
||||||
// The rendezvous ICE route is reachable without a prior punch and the peer decides how many
|
// The rendezvous ICE route is reachable without a prior punch and the peer decides how many
|
||||||
// candidates it sends, so these sites would let someone else set how much this machine writes to
|
// candidates it sends, so these sites would let someone else set how much this machine writes to
|
||||||
// its log file. One line a minute each, carrying the suppressed count.
|
// its log file. One line a minute each, carrying the suppressed count.
|
||||||
@@ -70,6 +75,45 @@ static REJECTED_REMOTE_ICE_LOG: hbb_common::log_throttle::LogThrottle =
|
|||||||
static FULL_ICE_QUEUE_LOG: hbb_common::log_throttle::LogThrottle =
|
static FULL_ICE_QUEUE_LOG: hbb_common::log_throttle::LogThrottle =
|
||||||
hbb_common::log_throttle::LogThrottle::new(ICE_LOG_INTERVAL);
|
hbb_common::log_throttle::LogThrottle::new(ICE_LOG_INTERVAL);
|
||||||
|
|
||||||
|
struct IceRoute {
|
||||||
|
tx: mpsc::Sender<String>,
|
||||||
|
recent: VecDeque<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IceRoute {
|
||||||
|
fn new(tx: mpsc::Sender<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
tx,
|
||||||
|
recent: VecDeque::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keeps `queue` the only way onto the channel, so nothing reaches it unrecorded.
|
||||||
|
fn is_same_channel(&self, other: &mpsc::Sender<String>) -> bool {
|
||||||
|
self.tx.same_channel(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skip the controller's re-send of a candidate already queued: the ICE agent that dedups
|
||||||
|
/// repeats is downstream of this queue, so the copy would spend a slot of its own.
|
||||||
|
/// False means the candidate was dropped.
|
||||||
|
fn queue(&mut self, candidate: String) -> bool {
|
||||||
|
let digest = ICE_DIGEST_STATE.hash_one(candidate.as_str());
|
||||||
|
if self.recent.contains(&digest) {
|
||||||
|
// Only honest about the drop if the route is still alive to have taken it.
|
||||||
|
return !self.tx.is_closed();
|
||||||
|
}
|
||||||
|
// Recorded once queued, never before: a refused candidate stays repairable by the re-send.
|
||||||
|
if self.tx.try_send(candidate).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if self.recent.len() >= ICE_DEDUP_WINDOW {
|
||||||
|
self.recent.pop_front();
|
||||||
|
}
|
||||||
|
self.recent.push_back(digest);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static SHOULD_EXIT: AtomicBool = AtomicBool::new(false);
|
static SHOULD_EXIT: AtomicBool = AtomicBool::new(false);
|
||||||
static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false);
|
static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false);
|
||||||
static SENT_REGISTER_PK: AtomicBool = AtomicBool::new(false);
|
static SENT_REGISTER_PK: AtomicBool = AtomicBool::new(false);
|
||||||
@@ -422,19 +466,27 @@ impl RendezvousMediator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Some(rendezvous_message::Union::IceCandidate(ice)) => {
|
Some(rendezvous_message::Union::IceCandidate(ice)) => {
|
||||||
let tx = WEBRTC_ICE_TXS.lock().await.get(&ice.session_key).cloned();
|
let queued = {
|
||||||
if let Some(tx) = tx {
|
let mut txs = WEBRTC_ICE_TXS.lock().await;
|
||||||
if tx.try_send(ice.candidate).is_err() {
|
txs.get_mut(&ice.session_key)
|
||||||
|
.map(|route| route.queue(ice.candidate))
|
||||||
|
};
|
||||||
|
match queued {
|
||||||
|
Some(false) => {
|
||||||
if let Some(n) = FULL_ICE_QUEUE_LOG.due() {
|
if let Some(n) = FULL_ICE_QUEUE_LOG.due() {
|
||||||
log::debug!("dropped {} ICE candidate(s): queue full or closed", n);
|
log::debug!("dropped {} ICE candidate(s): queue full or closed", n);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(n) = UNKNOWN_ICE_SESSION_LOG.due() {
|
None => {
|
||||||
log::debug!(
|
if let Some(n) = UNKNOWN_ICE_SESSION_LOG.due() {
|
||||||
"dropped {} ICE candidate(s) for unknown WebRTC session key, last: {}",
|
log::debug!(
|
||||||
n,
|
"dropped {} ICE candidate(s) for unknown WebRTC session key, last: {}",
|
||||||
ice.session_key
|
n,
|
||||||
);
|
ice.session_key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(rendezvous_message::Union::ConfigureUpdate(cu)) => {
|
Some(rendezvous_message::Union::ConfigureUpdate(cu)) => {
|
||||||
@@ -714,17 +766,17 @@ impl RendezvousMediator {
|
|||||||
return Ok(answer);
|
return Ok(answer);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bounded, like the controller's own candidate buffer: how many candidates arrive is the
|
// Bounded: how many candidates arrive is the sender's choice, while draining one costs a
|
||||||
// sender's choice, while draining one costs a JSON parse and the ICE agent's lock, so an
|
// JSON parse and the ICE agent's lock, so an unbounded queue lets whoever can reach this
|
||||||
// unbounded queue lets whoever can reach this session's route grow it without limit inside
|
// session's route grow it without limit inside a long-lived service process. A full queue
|
||||||
// a long-lived service process. A full queue drops the newest candidate, which costs at
|
// drops the newest candidate, and the controller re-sends it once — the digests beside the
|
||||||
// most one path; ICE keeps whatever pairs it already has.
|
// sender are what keep that re-send from spending a slot of its own.
|
||||||
let (remote_ice_tx, mut remote_ice_rx) = mpsc::channel::<String>(MAX_PENDING_REMOTE_ICE);
|
let (remote_ice_tx, mut remote_ice_rx) = mpsc::channel::<String>(MAX_PENDING_REMOTE_ICE);
|
||||||
let own_ice_tx = remote_ice_tx.clone();
|
let own_ice_tx = remote_ice_tx.clone();
|
||||||
WEBRTC_ICE_TXS
|
WEBRTC_ICE_TXS
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.insert(session_key.clone(), remote_ice_tx);
|
.insert(session_key.clone(), IceRoute::new(remote_ice_tx));
|
||||||
|
|
||||||
let stream_for_remote_ice = stream.clone();
|
let stream_for_remote_ice = stream.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
@@ -806,7 +858,7 @@ impl RendezvousMediator {
|
|||||||
let mut txs = WEBRTC_ICE_TXS.lock().await;
|
let mut txs = WEBRTC_ICE_TXS.lock().await;
|
||||||
if txs
|
if txs
|
||||||
.get(&session_key_for_cleanup)
|
.get(&session_key_for_cleanup)
|
||||||
.is_some_and(|tx| tx.same_channel(&own_ice_tx))
|
.is_some_and(|route| route.is_same_channel(&own_ice_tx))
|
||||||
{
|
{
|
||||||
txs.remove(&session_key_for_cleanup);
|
txs.remove(&session_key_for_cleanup);
|
||||||
}
|
}
|
||||||
@@ -1236,3 +1288,73 @@ impl Drop for CheckIfResendPk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{mpsc, IceRoute, ICE_DEDUP_WINDOW, MAX_PENDING_REMOTE_ICE};
|
||||||
|
|
||||||
|
fn queue(route: &mut IceRoute, candidate: &str) -> bool {
|
||||||
|
route.queue(candidate.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_re_sent_copy_does_not_spend_a_queue_slot() {
|
||||||
|
// Two slots, three sends: without the dedup the re-send takes the second and "relay",
|
||||||
|
// the one that traverses NAT, is the one refused.
|
||||||
|
let (tx, mut rx) = mpsc::channel::<String>(2);
|
||||||
|
let mut route = IceRoute::new(tx);
|
||||||
|
for _ in 0..2 {
|
||||||
|
assert!(queue(&mut route, "host"));
|
||||||
|
}
|
||||||
|
assert!(queue(&mut route, "relay"));
|
||||||
|
let mut queued = Vec::new();
|
||||||
|
while let Ok(candidate) = rx.try_recv() {
|
||||||
|
queued.push(candidate);
|
||||||
|
}
|
||||||
|
assert_eq!(queued, vec!["host".to_owned(), "relay".to_owned()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_candidate_the_full_queue_refused_is_not_remembered() {
|
||||||
|
let (tx, mut rx) = mpsc::channel::<String>(1);
|
||||||
|
let mut route = IceRoute::new(tx);
|
||||||
|
assert!(queue(&mut route, "host"));
|
||||||
|
assert!(!queue(&mut route, "relay"));
|
||||||
|
// The re-send is the only repair for a refused candidate; remembering it would swallow it.
|
||||||
|
assert_eq!(rx.try_recv().ok(), Some("host".to_owned()));
|
||||||
|
assert!(queue(&mut route, "relay"));
|
||||||
|
assert_eq!(rx.try_recv().ok(), Some("relay".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_re_send_is_skipped_while_the_original_is_still_queued() {
|
||||||
|
let (tx, mut rx) = mpsc::channel::<String>(MAX_PENDING_REMOTE_ICE);
|
||||||
|
let mut route = IceRoute::new(tx);
|
||||||
|
for i in 0..MAX_PENDING_REMOTE_ICE {
|
||||||
|
assert!(queue(&mut route, &format!("candidate-{}", i)));
|
||||||
|
}
|
||||||
|
assert!(queue(&mut route, "candidate-0"));
|
||||||
|
let mut queued = 0;
|
||||||
|
while rx.try_recv().is_ok() {
|
||||||
|
queued += 1;
|
||||||
|
}
|
||||||
|
assert_eq!(queued, MAX_PENDING_REMOTE_ICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_window_forgets_in_arrival_order() {
|
||||||
|
let (tx, mut rx) = mpsc::channel::<String>(MAX_PENDING_REMOTE_ICE);
|
||||||
|
let mut route = IceRoute::new(tx);
|
||||||
|
for i in 0..=ICE_DEDUP_WINDOW {
|
||||||
|
assert!(queue(&mut route, &format!("candidate-{}", i)));
|
||||||
|
assert!(rx.try_recv().is_ok());
|
||||||
|
}
|
||||||
|
// The oldest digest made room for the newest, so its re-send is admitted again.
|
||||||
|
assert!(queue(&mut route, "candidate-0"));
|
||||||
|
assert!(rx.try_recv().is_ok());
|
||||||
|
// A recent one is still skipped.
|
||||||
|
let recent = format!("candidate-{}", ICE_DEDUP_WINDOW);
|
||||||
|
assert!(queue(&mut route, &recent));
|
||||||
|
assert!(rx.try_recv().is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user