kcp: recover from a silent peer on the endpoint's own clock

KCP is the other transport with nothing to receive when the peer dies, and it
was the slower of the two: its endpoint reaps a connection only after 60s
without a packet, which is past the 30s inactivity timeout above it, so in
practice nothing but that timeout ever noticed.

The endpoint already tracks when each connection last heard from its peer and
now exposes it, so this reads that rather than anything derived from the session
loop - it keeps answering while that loop is busy sending. Its liveness ping now
goes out about every 2s rather than every 10s, so silence means the peer rather
than an idle link, and eight seconds of it is several missed pings.

Same threshold and the same recovery as the WebRTC half, so a user sees the same
thing on either transport.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns
This commit is contained in:
rustdesk
2026-09-08 15:05:00 +08:00
parent 2c613b949f
commit 8665a45673
3 changed files with 31 additions and 12 deletions

2
Cargo.lock generated
View File

@@ -4280,7 +4280,7 @@ dependencies = [
[[package]]
name = "kcp-sys"
version = "0.1.0"
source = "git+https://github.com/rustdesk-org/kcp-sys?branch=rustdesk-patches#023a0065398968989f2ddfcf5cc72bb886d02675"
source = "git+https://github.com/rustdesk-org/kcp-sys?branch=rustdesk-patches#938eda3e5e9757a612385503af7a6cb1189b2cdd"
dependencies = [
"anyhow",
"auto_impl",

View File

@@ -16,18 +16,22 @@ use crate::{
const RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT: Duration = Duration::from_secs(5);
const KCP_CLOSE_REASON_FLUSH_DELAY: Duration = Duration::from_millis(30);
// A peer that is killed, logged out or rebooted sends nothing at all over UDP, so the session
// sees silence and only a timeout ends it. This bounds that wait for WebRTC, which has a liveness
// signal of its own; TCP and WebSocket are left exactly as they were, and the 30s timeout below
// still backs all of them.
// sees silence and only a timeout ends it. These bound that wait for the two transports that
// have a liveness signal of their own; TCP and WebSocket are left exactly as they were, and the
// 30s timeout below still backs all of them.
//
// Not a hard upper bound on how soon this notices: sends are awaited inline in this same loop, so
// one in progress keeps the tick below from running, capped by the transport's own send timeout.
// Removing that needs the framing work that would stop one message owning the link.
// Neither is a hard upper bound on how soon this notices: sends are awaited inline in this same
// loop, so one in progress keeps the tick below from running. WebRTC caps that at its existing
// send timeout; KCP sets none, so a large message can delay the check by however long it takes
// to drain. Removing that needs the framing work that would stop one message owning the link.
//
// Grace after ICE reports Disconnected, which it does ~5s after it stops hearing from the peer,
// for ~8s in total. Disconnected is transient by design, so this waits out a Wi-Fi roam or a
// sleep/wake rather than acting on the first hint.
const WEBRTC_SUSPECT_GRACE: Duration = Duration::from_secs(3);
// KCP has no equivalent hint, only how long since a packet last arrived; its endpoint pings an
// idle peer about every 2s, so this is several missed pings.
const KCP_PEER_SILENCE_LIMIT: Duration = Duration::from_secs(8);
#[cfg(feature = "unix-file-copy-paste")]
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
use base::{
@@ -339,7 +343,11 @@ impl<T: InvokeUiSession> Remote<T> {
webrtc_suspect_since = None;
}
let peer_gone = webrtc_suspect_since
.map_or(false, |since| since.elapsed() >= WEBRTC_SUSPECT_GRACE);
.map_or(false, |since| since.elapsed() >= WEBRTC_SUSPECT_GRACE)
|| kcp
.as_ref()
.and_then(|k| k.peer_silent_for())
.map_or(false, |silent| silent >= KCP_PEER_SILENCE_LIMIT);
if peer_gone {
log::info!("Peer stopped answering, reconnecting");
self.handler.msgbox("restarting-show", "Connecting...", "Connection in progress. Please wait.", "");

View File

@@ -8,14 +8,15 @@ use hbb_common::{
tokio_util, ResultType, Stream,
};
use kcp_sys::{
endpoint::KcpEndpoint,
endpoint::{ConnId, KcpEndpoint},
packet_def::{KcpPacket, KcpPacketHeader},
stream,
};
use std::{net::SocketAddr, sync::Arc};
pub struct KcpStream {
_endpoint: KcpEndpoint,
endpoint: KcpEndpoint,
conn_id: ConnId,
stop_sender: Option<oneshot::Sender<()>>,
}
@@ -41,6 +42,14 @@ impl KcpStream {
}
}
/// How long since a valid packet was last received from the peer, or `None` once the
/// connection is gone. Answered by the KCP endpoint's own tasks, not by the session's read
/// loop, so it stays meaningful while that loop is busy sending a large message; and the
/// endpoint pings an idle peer often enough that silence here means the peer, not quiet.
pub fn peer_silent_for(&self) -> Option<std::time::Duration> {
self.endpoint.peer_silent_for(&self.conn_id)
}
fn create_framed(stream: stream::KcpStream, local_addr: Option<SocketAddr>) -> Stream {
Stream::Tcp(FramedStream(
tokio_util::codec::Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
@@ -77,7 +86,8 @@ impl KcpStream {
if let Some(stream) = stream::KcpStream::new(&endpoint, conn_id) {
Ok((
Self {
_endpoint: endpoint,
endpoint,
conn_id,
stop_sender: Some(stop_sender),
},
Self::create_framed(stream, udp_socket.local_addr().ok()),
@@ -108,7 +118,8 @@ impl KcpStream {
if let Some(stream) = stream::KcpStream::new(&endpoint, conn_id) {
Ok((
Self {
_endpoint: endpoint,
endpoint,
conn_id,
stop_sender: Some(stop_sender),
},
Self::create_framed(stream, udp_socket.local_addr().ok()),