From 4b32010560ca941110c74a6c07138b7f061bbf63 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 19 Aug 2026 12:36:01 +0800 Subject: [PATCH] fix(rdp): isolate credentials across concurrent sessions Signed-off-by: 21pages --- src/platform/windows.rs | 106 ++++++++++++++++++++++++++++++++++++---- src/port_forward.rs | 64 ++++++++++++++++++++---- 2 files changed, 151 insertions(+), 19 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 083d774a7..910c842f9 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -2644,6 +2644,49 @@ pub fn wide_string(s: &str) -> Vec { const RDP_CREDENTIAL_COMMENT_PREFIX: &str = "RustDesk temporary RDP credential "; +#[derive(Clone)] +pub struct RdpLoopbackAddress(Arc); + +struct RdpLoopbackAddressInner { + index: u8, + host: String, +} + +impl RdpLoopbackAddress { + pub fn bind_host(&self) -> &str { + &self.0.host + } + + pub fn mstsc_host(&self) -> &str { + &self.0.host + } +} + +impl Drop for RdpLoopbackAddressInner { + fn drop(&mut self) { + rdp_loopback_addresses().lock().unwrap().remove(&self.index); + } +} + +fn rdp_loopback_addresses() -> &'static Mutex> { + static ADDRESSES: std::sync::OnceLock>> = + std::sync::OnceLock::new(); + ADDRESSES.get_or_init(|| Mutex::new(std::collections::HashSet::new())) +} + +pub fn reserve_rdp_loopback_address() -> ResultType { + let mut addresses = rdp_loopback_addresses().lock().unwrap(); + let index = (2..=254) + .find(|index| !addresses.contains(index)) + .ok_or_else(|| anyhow!("No RDP loopback address is available"))?; + addresses.insert(index); + let host = format!("127.0.0.{}", index); + Ok(RdpLoopbackAddress(Arc::new(RdpLoopbackAddressInner { + index, + host, + }))) +} + struct RdpCredentialBuffer(*mut windows::Win32::Security::Credentials::CREDENTIALW); // CredReadW returns a self-contained allocation that remains valid until @@ -2941,26 +2984,34 @@ fn sanitize_rdp_window_title(name: &str) -> String { .collect() } -fn should_update_rdp_window_title(current: &str, name: &str, applied_name: &str) -> bool { +fn should_update_rdp_window_title( + current: &str, + name: &str, + applied_name: &str, + endpoint: &str, +) -> bool { current != name - && (current.contains("localhost") || (!applied_name.is_empty() && current == applied_name)) + && (current.contains(endpoint) || (!applied_name.is_empty() && current == applied_name)) } pub fn set_rdp_window_title( mut child: std::process::Child, name_of: F, + endpoint: String, _credential: Option, + _loopback: Option, ) where F: Fn() -> String + Send + 'static, { let process_id = child.id(); - // mstsc owns the title and can restore "localhost" while connecting or + // mstsc owns the title and can restore the endpoint while connecting or // reconnecting. Follow only the process we launched and reapply the peer // name until it exits, so concurrent RDP sessions cannot rename each other. if let Err(err) = std::thread::Builder::new() .name("rdp-window-title".to_owned()) .spawn(move || { let _credential = _credential; + let _loopback = _loopback; let mut warned = false; let mut applied_name = String::new(); loop { @@ -2973,7 +3024,12 @@ pub fn set_rdp_window_title( Ok(None) => { let name = sanitize_rdp_window_title(&name_of()); if !name.is_empty() { - match set_process_rdp_window_title(process_id, &name, &applied_name) { + match set_process_rdp_window_title( + process_id, + &name, + &applied_name, + &endpoint, + ) { Ok(applied) => { if applied { applied_name = name; @@ -3001,11 +3057,13 @@ fn set_process_rdp_window_title( process_id: DWORD, name: &str, applied_name: &str, + endpoint: &str, ) -> io::Result { struct Context<'a> { process_id: DWORD, name: &'a str, applied_name: &'a str, + endpoint: &'a str, title: Vec, applied: bool, error: Option, @@ -3032,7 +3090,12 @@ fn set_process_rdp_window_title( context.applied = true; return TRUE; } - if !should_update_rdp_window_title(¤t, context.name, context.applied_name) { + if !should_update_rdp_window_title( + ¤t, + context.name, + context.applied_name, + context.endpoint, + ) { return TRUE; } let mut result = 0; @@ -3071,6 +3134,7 @@ fn set_process_rdp_window_title( process_id, name, applied_name, + endpoint, title: wide_string(name), applied: false, error: None, @@ -5009,25 +5073,49 @@ mod tests { assert!(should_update_rdp_window_title( "localhost - Remote Desktop Connection", "peer", - "" + "", + "localhost" + )); + assert!(should_update_rdp_window_title( + "127.0.0.2 - Remote Desktop Connection", + "peer", + "", + "127.0.0.2" )); assert!(should_update_rdp_window_title( "peer", "peer (hostname)", - "peer" + "peer", + "127.0.0.2" )); assert!(!should_update_rdp_window_title( "localhost tunnel", "localhost tunnel", - "localhost tunnel" + "localhost tunnel", + "localhost" )); assert!(!should_update_rdp_window_title( "Remote Desktop Connection", "peer", - "" + "", + "localhost" )); } + #[test] + fn rdp_loopback_addresses_are_unique_while_reserved() { + let first = reserve_rdp_loopback_address().unwrap(); + let second = reserve_rdp_loopback_address().unwrap(); + assert_eq!(first.bind_host(), "127.0.0.2"); + assert_eq!(first.mstsc_host(), "127.0.0.2"); + assert_eq!(second.bind_host(), "127.0.0.3"); + assert_eq!(second.mstsc_host(), "127.0.0.3"); + + drop(first); + let reused = reserve_rdp_loopback_address().unwrap(); + assert_eq!(reused.bind_host(), "127.0.0.2"); + } + // Test-only reusable Win32 HANDLE RAII helper. // If a future non-test path needs the same pattern, move it out of this test module. // diff --git a/src/port_forward.rs b/src/port_forward.rs index b40c3361e..4940caf7d 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -16,18 +16,30 @@ use hbb_common::{ }; #[cfg(windows)] -const RDP_CREDENTIAL_TARGET: &str = "TERMSRV/localhost"; +type RdpLoopbackLease = crate::platform::RdpLoopbackAddress; -fn run_rdp(port: u16, lc: &Arc>, id: &str) { +#[cfg(not(windows))] +#[derive(Clone)] +struct RdpLoopbackLease; + +fn run_rdp( + port: u16, + host: &str, + lc: &Arc>, + id: &str, + loopback: Option, +) { #[cfg(windows)] let (username, password) = { let lc = lc.read().unwrap(); (lc.get_option("rdp_username"), lc.get_option("rdp_password")) }; #[cfg(windows)] + let credential_target = format!("TERMSRV/{}", host); + #[cfg(windows)] let (rdp_credential, prompt_for_credentials) = match crate::platform::prepare_temporary_rdp_credential( - RDP_CREDENTIAL_TARGET, + &credential_target, &username, &password, ) { @@ -35,7 +47,7 @@ fn run_rdp(port: u16, lc: &Arc>, id: &str) { Err(err) => { log::warn!( "Failed to prepare RDP credential for target '{}': {}", - RDP_CREDENTIAL_TARGET, + credential_target, err ); (None, true) @@ -44,7 +56,7 @@ fn run_rdp(port: u16, lc: &Arc>, id: &str) { // Keep using /v instead of a generated .rdp file: mstsc then preserves the // user's Default.rdp settings and avoids unsigned-file warnings or policies. let mut command = std::process::Command::new("mstsc"); - command.arg(format!("/v:localhost:{}", port)); + command.arg(format!("/v:{}:{}", host, port)); #[cfg(windows)] if prompt_for_credentials { command.arg("/prompt"); @@ -58,11 +70,13 @@ fn run_rdp(port: u16, lc: &Arc>, id: &str) { crate::platform::set_rdp_window_title( child, move || rdp_display_name(&lc, &id), + host.to_owned(), rdp_credential, + loopback, ); } #[cfg(not(windows))] - let _ = (child, lc, id); + let _ = (child, lc, id, loopback); } Err(err) => log::warn!("Failed to launch mstsc: {}", err), } @@ -97,12 +111,42 @@ pub async fn listen( remote_host: String, remote_port: i32, ) -> ResultType<()> { - let listener = tcp::new_listener(format!("127.0.0.1:{}", port), true).await?; + let is_rdp = port == 0; + #[cfg(windows)] + let rdp_loopback = if is_rdp { + let has_username = { + let lc = lc.read().unwrap(); + !lc.get_option("rdp_username").is_empty() + }; + if has_username { + Some(crate::platform::reserve_rdp_loopback_address()?) + } else { + None + } + } else { + None + }; + #[cfg(not(windows))] + let rdp_loopback: Option = None; + #[cfg(windows)] + let listener_host = rdp_loopback + .as_ref() + .map(|address| address.bind_host()) + .unwrap_or("127.0.0.1"); + #[cfg(not(windows))] + let listener_host = "127.0.0.1"; + let listener = tcp::new_listener(format!("{}:{}", listener_host, port), true).await?; let addr = listener.local_addr()?; log::info!("listening on port {:?}", addr); - let is_rdp = port == 0; + #[cfg(windows)] + let rdp_host = rdp_loopback + .as_ref() + .map(|address| address.mstsc_host()) + .unwrap_or("localhost"); + #[cfg(not(windows))] + let rdp_host = "localhost"; if is_rdp { - run_rdp(addr.port(), &lc, &id); + run_rdp(addr.port(), rdp_host, &lc, &id, rdp_loopback.clone()); } let mut ui_receiver = ui_receiver; loop { @@ -140,7 +184,7 @@ pub async fn listen( } Some(Data::NewRDP) => { println!("receive run_rdp from ui_receiver"); - run_rdp(addr.port(), &lc, &id); + run_rdp(addr.port(), rdp_host, &lc, &id, rdp_loopback.clone()); } _ => {} }