From 01dbb764996f9f88d2cb7bcc06e26f86ce8396db Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:06:24 +0800 Subject: [PATCH] server: do not lock the screen for a connection a reconnect replaced (#16124) A controlling peer whose link dies without a close reconnects, while the connection it left behind runs on here until its own 30s inactivity timeout. That one then ends with `on_close("Timeout", true)`, and the lock is gated only on `lock_after_session_end` and this connection's own `keyboard` - both set by the very controller that is at that moment working in the session its reconnect re-established. Nothing anywhere asks whether the session is still being controlled, so the screen locks under a peer that came back twenty-odd seconds earlier, and the operator's desk locks itself in front of them. The lock now also requires that no newer remote control connection of this session is authorized. Newer, not merely other. A connection stays in `AUTHED_CONNS` until its `AuthedConnID` drops, which is well after `on_close` returns, so a symmetric test would have two of one session ending together each see the other and neither lock. Ids come from a counter, so `>` orders them: of a session's connections the last still locks, whether they end one after another or at once. Remote control only, and this session only. The other kinds do not keep a screen in use, and `send_logon_response` clears `keyboard` for a file transfer, a terminal and a camera view, so none of those reaches the gate at all - a port forward keeps it, and is kept out only by the client not sending `lock_after_session_end` on one. Another peer's session is left exactly as it is: whether its ending locks the screen while this one is connected is a separate question, and not one a timeout on this side should start answering. Every close that asked to lock, not only the timeout. A connection its own peer has already replaced should not lock the session that replaced it however it ends, and singling out one reason would leave the same race reachable through the others. Two things it does not cover. A reconnect that has not authorized yet is not in `AUTHED_CONNS`, so a timeout landing while one waits at the accept prompt or on 2FA locks as before. And the lock is skipped, not handed on: if the connection that replaced this one later ends through a path that asks not to lock - a failed send, a stopped service - nothing locks. That is what those paths already choose for a connection dying of a network error, on the assumption that the peer will retry, and it is the same assumption this makes. Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns Co-authored-by: Claude Opus 5 --- src/server/connection.rs | 55 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index fc61420dd..24f5bf537 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5097,7 +5097,11 @@ impl Connection { // But it's not necessary now and we have to consider two audio services(client, server). crate::audio_service::set_voice_call_input_device(None, true); log::info!("#{} Connection closed: {}", self.inner.id(), reason); - if lock && self.lock_after_session_end && self.keyboard { + if lock + && self.lock_after_session_end + && self.keyboard + && !raii::AuthedConnID::session_reconnected(self.inner.id(), &self.session_key()) + { #[cfg(not(any(target_os = "android", target_os = "ios")))] lock_screen().await; } @@ -6658,6 +6662,21 @@ mod raii { pub struct AuthedConnID(i32, AuthConnType); impl AuthedConnID { + pub(super) fn is_newer_session_remote(c: &AuthedConn, id: i32, key: &SessionKey) -> bool { + c.conn_id > id && c.conn_type == AuthConnType::Remote && &c.session_key == key + } + + /// Whether a newer remote control connection of this session has replaced this one. A + /// controlling peer whose link dies reconnects while the connection it left behind runs + /// on here until its own timeout; locking for that one would lock a session that has + /// already resumed on its replacement. + pub fn session_reconnected(id: i32, key: &SessionKey) -> bool { + let conns = AUTHED_CONNS.lock().unwrap(); + conns + .iter() + .any(|c| Self::is_newer_session_remote(c, id, key)) + } + pub fn new( conn_id: i32, conn_type: AuthConnType, @@ -7547,4 +7566,38 @@ mod test { Ok(BoolOption::NotSet) ); } + #[test] + fn only_a_newer_remote_control_of_the_same_session_keeps_the_screen_unlocked() { + let replaced_by = super::raii::AuthedConnID::is_newer_session_remote; + + let key = |session_id, peer: &str| SessionKey { + peer_id: peer.to_owned(), + name: "".to_owned(), + session_id, + }; + let conn = |conn_id, conn_type, session_key| AuthedConn { + conn_id, + conn_type, + session_key, + sender: mpsc::unbounded_channel().0, + printer: false, + }; + let mine = key(7, "peer"); + let remote = AuthConnType::Remote; + + assert!(replaced_by(&conn(3, remote, mine.clone()), 2, &mine)); + // An older one, and itself: of connections ending at once only the last still locks. + assert!(!replaced_by(&conn(1, remote, mine.clone()), 2, &mine)); + assert!(!replaced_by(&conn(2, remote, mine.clone()), 2, &mine)); + // A kind that keeps no screen in use. + assert!(!replaced_by( + &conn(3, AuthConnType::Terminal, mine.clone()), + 2, + &mine + )); + // Another session of this peer, and another peer on the same session id: `SessionKey` + // is all three fields, and either of those is someone else's screen to lock. + assert!(!replaced_by(&conn(3, remote, key(8, "peer")), 2, &mine)); + assert!(!replaced_by(&conn(3, remote, key(7, "other")), 2, &mine)); + } }