mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-11 23:11:01 +03:00
fix(client): allow switch-sides back-connection in incoming-only mode (#15780)
* fix(client): allow switch-sides back-connection in incoming-only mode "Switch sides" makes the controlled client run `--connect <peer> --switch_uuid <uuid>`, which Client::_start rejected outright in incoming-only custom clients, so the feature silently dropped the session and never switched. Exempt exactly that back-connection: a default-conn session carrying a switch uuid may proceed. The uuid is then verified against the local server process in handle_hash(); if it is missing there (forged or expired), an incoming-only client now aborts with an error instead of falling through to password login, so the outgoing-connection restriction cannot be bypassed with a crafted --switch_uuid. Fixes rustdesk/rustdesk#11200 (discussion) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(client): validate switch-back grants before connecting - check pending peer/UUID grants before bypassing incoming-only mode - close rejected switch-back connections and suppress retries - keep grant consumption in handle_hash and test non-consuming checks Signed-off-by: 21pages <sunboeasy@gmail.com> * fix(client): prevent switch-back UUID reuse - claim pending switch-back grants before connecting - retain claimed grants to reject duplicate requests - bind authorization to the peer ID and UUID - use a shared TTL for switch-back grants Signed-off-by: 21pages <sunboeasy@gmail.com> * fix(client): defer switch UUID consumption until authentication Signed-off-by: 21pages <sunboeasy@gmail.com> * fix(client): reject repeated hash login in incoming-only mode Signed-off-by: 21pages <sunboeasy@gmail.com> --------- Signed-off-by: 21pages <sunboeasy@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
@@ -252,7 +252,7 @@ impl Client {
|
|||||||
(i32, String),
|
(i32, String),
|
||||||
bool,
|
bool,
|
||||||
)> {
|
)> {
|
||||||
if config::is_incoming_only() {
|
if config::is_incoming_only() && !is_switch_sides_back(conn_type, &interface).await {
|
||||||
bail!("Incoming only mode");
|
bail!("Incoming only mode");
|
||||||
}
|
}
|
||||||
// to-do: remember the port for each peer, so that we can retry easier
|
// to-do: remember the port for each peer, so that we can retry easier
|
||||||
@@ -3455,9 +3455,55 @@ pub fn handle_login_error(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "Switch sides" requires the incoming-only client to connect back to its
|
||||||
|
// controlling peer; verify the local pending uuid before opening the connection.
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
async fn consume_local_switch_sides_uuid(id: &str, uuid: &Uuid) -> bool {
|
async fn is_switch_sides_back(conn_type: ConnType, interface: &impl Interface) -> bool {
|
||||||
|
if conn_type != ConnType::DEFAULT_CONN {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let (id, uuid) = {
|
||||||
|
let lch = interface.get_lch();
|
||||||
|
let lc = lch.read().unwrap();
|
||||||
|
let Some(uuid) = lc.switch_uuid.as_deref() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Ok(uuid) = Uuid::parse_str(uuid) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
(lc.id.clone(), uuid)
|
||||||
|
};
|
||||||
|
if !request_local_switch_sides_uuid(
|
||||||
|
&id,
|
||||||
|
&uuid,
|
||||||
|
crate::ipc::SwitchSidesUuidAction::Check,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let lch = interface.get_lch();
|
||||||
|
let lc = lch.read().unwrap();
|
||||||
|
let current_uuid = lc
|
||||||
|
.switch_uuid
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|value| Uuid::parse_str(value).ok());
|
||||||
|
lc.id == id && current_uuid.as_ref() == Some(&uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(all(feature = "flutter", not(any(target_os = "android", target_os = "ios")))))]
|
||||||
|
async fn is_switch_sides_back(_conn_type: ConnType, _interface: &impl Interface) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
async fn request_local_switch_sides_uuid(
|
||||||
|
id: &str,
|
||||||
|
uuid: &Uuid,
|
||||||
|
action: crate::ipc::SwitchSidesUuidAction,
|
||||||
|
) -> bool {
|
||||||
let Ok(mut conn) = crate::ipc::connect(1000, "").await else {
|
let Ok(mut conn) = crate::ipc::connect(1000, "").await else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -3466,6 +3512,7 @@ async fn consume_local_switch_sides_uuid(id: &str, uuid: &Uuid) -> bool {
|
|||||||
.send(&crate::ipc::Data::SwitchSidesUuid(
|
.send(&crate::ipc::Data::SwitchSidesUuid(
|
||||||
uuid.clone(),
|
uuid.clone(),
|
||||||
id.to_owned(),
|
id.to_owned(),
|
||||||
|
action,
|
||||||
None,
|
None,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
@@ -3477,9 +3524,10 @@ async fn consume_local_switch_sides_uuid(id: &str, uuid: &Uuid) -> bool {
|
|||||||
Ok(Some(crate::ipc::Data::SwitchSidesUuid(
|
Ok(Some(crate::ipc::Data::SwitchSidesUuid(
|
||||||
returned_uuid,
|
returned_uuid,
|
||||||
returned_id,
|
returned_id,
|
||||||
|
returned_action,
|
||||||
Some(true),
|
Some(true),
|
||||||
))) => {
|
))) => {
|
||||||
returned_uuid == uuid && returned_id == id
|
returned_uuid == uuid && returned_id == id && returned_action == action
|
||||||
}
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
@@ -3512,7 +3560,13 @@ pub async fn handle_hash(
|
|||||||
if let Some(uuid) = uuid {
|
if let Some(uuid) = uuid {
|
||||||
if let Ok(uuid) = uuid::Uuid::from_str(&uuid) {
|
if let Ok(uuid) = uuid::Uuid::from_str(&uuid) {
|
||||||
let id = lc.read().unwrap().id.clone();
|
let id = lc.read().unwrap().id.clone();
|
||||||
if !consume_local_switch_sides_uuid(&id, &uuid).await {
|
if !request_local_switch_sides_uuid(
|
||||||
|
&id,
|
||||||
|
&uuid,
|
||||||
|
crate::ipc::SwitchSidesUuidAction::Consume,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
log::warn!("Ignored untrusted switch_uuid");
|
log::warn!("Ignored untrusted switch_uuid");
|
||||||
} else {
|
} else {
|
||||||
lc.write().unwrap().allow_switch_back_once();
|
lc.write().unwrap().allow_switch_back_once();
|
||||||
@@ -3522,6 +3576,19 @@ pub async fn handle_hash(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Incoming-only may connect out solely for a verified switch-back;
|
||||||
|
// never fall through to password login, including on repeated hashes.
|
||||||
|
if config::is_incoming_only() {
|
||||||
|
interface.msgbox("error", "Connection Error", "Incoming only mode", "");
|
||||||
|
let mut misc = Misc::new();
|
||||||
|
misc.set_close_reason(
|
||||||
|
"Connection not allowed in incoming-only mode".to_owned(),
|
||||||
|
);
|
||||||
|
let mut msg = Message::new();
|
||||||
|
msg.set_misc(misc);
|
||||||
|
allow_err!(peer.send(&msg).await);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// last password
|
// last password
|
||||||
let mut password = lc.read().unwrap().password.clone();
|
let mut password = lc.read().unwrap().password.clone();
|
||||||
@@ -4031,9 +4098,25 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b
|
|||||||
&& !text.to_lowercase().contains("mismatch")
|
&& !text.to_lowercase().contains("mismatch")
|
||||||
&& !text.to_lowercase().contains("manually")
|
&& !text.to_lowercase().contains("manually")
|
||||||
&& !text.to_lowercase().contains("restricted")
|
&& !text.to_lowercase().contains("restricted")
|
||||||
|
&& !text.to_lowercase().contains("incoming only")
|
||||||
&& !text.to_lowercase().contains("not allowed")))
|
&& !text.to_lowercase().contains("not allowed")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod retry_tests {
|
||||||
|
use super::check_if_retry;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn incoming_only_error_is_not_retryable() {
|
||||||
|
assert!(!check_if_retry(
|
||||||
|
"error",
|
||||||
|
"Connection Error",
|
||||||
|
"Incoming only mode",
|
||||||
|
false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn hc_connection(
|
pub async fn hc_connection(
|
||||||
feedback: i32,
|
feedback: i32,
|
||||||
rendezvous_server: String,
|
rendezvous_server: String,
|
||||||
|
|||||||
23
src/ipc.rs
23
src/ipc.rs
@@ -312,6 +312,14 @@ pub enum DataPortableService {
|
|||||||
CmShowElevation(bool),
|
CmShowElevation(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SwitchSidesUuidAction {
|
||||||
|
Check,
|
||||||
|
Consume,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
#[serde(tag = "t", content = "c")]
|
#[serde(tag = "t", content = "c")]
|
||||||
pub enum Data {
|
pub enum Data {
|
||||||
@@ -387,7 +395,7 @@ pub enum Data {
|
|||||||
SwitchSidesRequest(String),
|
SwitchSidesRequest(String),
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
SwitchSidesUuid(String, String, Option<bool>),
|
SwitchSidesUuid(String, String, SwitchSidesUuidAction, Option<bool>),
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
SwitchSidesBack,
|
SwitchSidesBack,
|
||||||
@@ -1050,14 +1058,21 @@ async fn handle(data: Data, stream: &mut Connection) {
|
|||||||
}
|
}
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
Data::SwitchSidesUuid(uuid, id, None) => {
|
Data::SwitchSidesUuid(uuid, id, action, None) => {
|
||||||
let allowed = uuid
|
let allowed = uuid
|
||||||
.parse::<uuid::Uuid>()
|
.parse::<uuid::Uuid>()
|
||||||
.map(|uuid| crate::server::remove_pending_switch_sides_uuid(&id, &uuid))
|
.map(|uuid| match action {
|
||||||
|
SwitchSidesUuidAction::Check => {
|
||||||
|
crate::server::has_pending_switch_sides_uuid(&id, &uuid)
|
||||||
|
}
|
||||||
|
SwitchSidesUuidAction::Consume => {
|
||||||
|
crate::server::claim_pending_switch_sides_uuid(&id, &uuid)
|
||||||
|
}
|
||||||
|
})
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
allow_err!(
|
allow_err!(
|
||||||
stream
|
stream
|
||||||
.send(&Data::SwitchSidesUuid(uuid, id, Some(allowed)))
|
.send(&Data::SwitchSidesUuid(uuid, id, action, Some(allowed)))
|
||||||
.await
|
.await
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,11 +91,15 @@ lazy_static::lazy_static! {
|
|||||||
static ref WAKELOCK_KEEP_AWAKE_OPTION: Arc::<Mutex<Option<bool>>> = Default::default();
|
static ref WAKELOCK_KEEP_AWAKE_OPTION: Arc::<Mutex<Option<bool>>> = Default::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
const SWITCH_SIDES_UUID_TTL: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref SWITCH_SIDES_UUID: Arc::<Mutex<HashMap<String, (Instant, uuid::Uuid)>>> = Default::default();
|
static ref SWITCH_SIDES_UUID: Arc::<Mutex<HashMap<String, (Instant, uuid::Uuid)>>> = Default::default();
|
||||||
static ref PENDING_SWITCH_SIDES_UUID: Arc::<Mutex<HashMap<String, (Instant, uuid::Uuid)>>> = Default::default();
|
static ref PENDING_SWITCH_SIDES_UUID: Arc::<Mutex<HashMap<String, (Instant, uuid::Uuid, bool)>>> = Default::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -3085,7 +3089,7 @@ impl Connection {
|
|||||||
SWITCH_SIDES_UUID
|
SWITCH_SIDES_UUID
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.retain(|_, v| v.0.elapsed() < Duration::from_secs(10));
|
.retain(|_, v| v.0.elapsed() < SWITCH_SIDES_UUID_TTL);
|
||||||
let uuid_old = SWITCH_SIDES_UUID.lock().unwrap().remove(&lr.my_id);
|
let uuid_old = SWITCH_SIDES_UUID.lock().unwrap().remove(&lr.my_id);
|
||||||
if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) {
|
if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) {
|
||||||
if let Some((_instant, uuid_old)) = uuid_old {
|
if let Some((_instant, uuid_old)) = uuid_old {
|
||||||
@@ -3825,17 +3829,18 @@ impl Connection {
|
|||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
Some(misc::Union::SwitchSidesRequest(s)) => {
|
Some(misc::Union::SwitchSidesRequest(s)) => {
|
||||||
if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) {
|
if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) {
|
||||||
crate::server::insert_pending_switch_sides_uuid(
|
if crate::server::insert_pending_switch_sides_uuid(
|
||||||
self.lr.my_id.clone(),
|
self.lr.my_id.clone(),
|
||||||
uuid.clone(),
|
uuid.clone(),
|
||||||
);
|
) {
|
||||||
crate::run_me(vec![
|
crate::run_me(vec![
|
||||||
"--connect",
|
"--connect",
|
||||||
&self.lr.my_id,
|
&self.lr.my_id,
|
||||||
"--switch_uuid",
|
"--switch_uuid",
|
||||||
uuid.to_string().as_ref(),
|
uuid.to_string().as_ref(),
|
||||||
])
|
])
|
||||||
.ok();
|
.ok();
|
||||||
|
}
|
||||||
self.on_close("switch sides", false).await;
|
self.on_close("switch sides", false).await;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -6139,23 +6144,40 @@ pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) {
|
|||||||
|
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn insert_pending_switch_sides_uuid(id: String, uuid: uuid::Uuid) {
|
pub fn insert_pending_switch_sides_uuid(id: String, uuid: uuid::Uuid) -> bool {
|
||||||
let mut uuids = PENDING_SWITCH_SIDES_UUID.lock().unwrap();
|
let mut uuids = PENDING_SWITCH_SIDES_UUID.lock().unwrap();
|
||||||
uuids.retain(|_, (instant, _)| instant.elapsed() < Duration::from_secs(10));
|
uuids.retain(|_, (instant, _, _)| instant.elapsed() < SWITCH_SIDES_UUID_TTL);
|
||||||
uuids.insert(id, (tokio::time::Instant::now(), uuid));
|
if uuids.get(&id).map(|(_, stored_uuid, _)| stored_uuid) == Some(&uuid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uuids.insert(id, (tokio::time::Instant::now(), uuid, false));
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn remove_pending_switch_sides_uuid(id: &str, uuid: &uuid::Uuid) -> bool {
|
pub fn has_pending_switch_sides_uuid(id: &str, uuid: &uuid::Uuid) -> bool {
|
||||||
let mut uuids = PENDING_SWITCH_SIDES_UUID.lock().unwrap();
|
let mut uuids = PENDING_SWITCH_SIDES_UUID.lock().unwrap();
|
||||||
uuids.retain(|_, (instant, _)| instant.elapsed() < Duration::from_secs(10));
|
uuids.retain(|_, (instant, _, _)| instant.elapsed() < SWITCH_SIDES_UUID_TTL);
|
||||||
if uuids.get(id).map(|(_, stored_uuid)| stored_uuid == uuid) == Some(true) {
|
uuids
|
||||||
uuids.remove(id);
|
.get(id)
|
||||||
true
|
.map(|(_, stored_uuid, claimed)| stored_uuid == uuid && !*claimed)
|
||||||
} else {
|
== Some(true)
|
||||||
false
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn claim_pending_switch_sides_uuid(id: &str, uuid: &uuid::Uuid) -> bool {
|
||||||
|
let mut uuids = PENDING_SWITCH_SIDES_UUID.lock().unwrap();
|
||||||
|
uuids.retain(|_, (instant, _, _)| instant.elapsed() < SWITCH_SIDES_UUID_TTL);
|
||||||
|
// Keep claimed entries until expiry so replaying a request cannot launch another connection.
|
||||||
|
if let Some((_, stored_uuid, claimed)) = uuids.get_mut(id) {
|
||||||
|
if stored_uuid == uuid && !*claimed {
|
||||||
|
*claimed = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
@@ -7094,6 +7116,26 @@ mod test {
|
|||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
#[test]
|
||||||
|
fn test_pending_switch_sides_uuid_is_claimed_once() {
|
||||||
|
let id = uuid::Uuid::new_v4().to_string();
|
||||||
|
let uuid = uuid::Uuid::new_v4();
|
||||||
|
let other_uuid = uuid::Uuid::new_v4();
|
||||||
|
assert!(insert_pending_switch_sides_uuid(id.clone(), uuid.clone()));
|
||||||
|
|
||||||
|
assert!(!insert_pending_switch_sides_uuid(id.clone(), uuid.clone()));
|
||||||
|
assert!(has_pending_switch_sides_uuid(&id, &uuid));
|
||||||
|
assert!(!has_pending_switch_sides_uuid(&id, &other_uuid));
|
||||||
|
assert!(!claim_pending_switch_sides_uuid("other-peer", &uuid));
|
||||||
|
assert!(!claim_pending_switch_sides_uuid(&id, &other_uuid));
|
||||||
|
assert!(claim_pending_switch_sides_uuid(&id, &uuid));
|
||||||
|
assert!(!has_pending_switch_sides_uuid(&id, &uuid));
|
||||||
|
assert!(!claim_pending_switch_sides_uuid(&id, &uuid));
|
||||||
|
assert!(!insert_pending_switch_sides_uuid(id, uuid));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn login_scope_latches_session_scope_across_login_retries() {
|
fn login_scope_latches_session_scope_across_login_retries() {
|
||||||
let port_forward = |host: &str| {
|
let port_forward = |host: &str| {
|
||||||
|
|||||||
Reference in New Issue
Block a user