From 18a966c28e7a00b1c2e13bcdfe4235df0fa87d6b Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 16 Sep 2026 13:33:22 +0800 Subject: [PATCH] feat: self-connection blocking (A connects to A) server-side solution --- docker-compose.yml | 2 +- src/lib.rs | 3 +- src/main.rs | 2 +- src/rendezvous_server.rs | 65 +++++++++++++++++++++++-- src/token_device.rs | 100 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 src/token_device.rs diff --git a/docker-compose.yml b/docker-compose.yml index 11608f8..eac4339 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: environment: - RELAY= - ENCRYPTED_ONLY=1 - - MUST_LOGIN=N + - MUST_LOGIN=Y - TZ=Asia/Shanghai - RUSTDESK_API_RUSTDESK_ID_SERVER= - RUSTDESK_API_RUSTDESK_RELAY_SERVER= diff --git a/src/lib.rs b/src/lib.rs index 78aa01f..2b9339d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,4 +4,5 @@ pub mod common; mod database; mod peer; mod version; -pub mod jwt; \ No newline at end of file +pub mod jwt; +mod token_device; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index f72b26a..205bfea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,7 @@ fn main() -> ResultType<()> { -M, --rmem=[NUMBER(default={RMEM})] 'Sets UDP recv buffer size, set system rmem_max first, e.g., sudo sysctl -w net.core.rmem_max=52428800. vi /etc/sysctl.conf, net.core.rmem_max=52428800, sudo sysctl –p' , --mask=[MASK] '[DEPRECATED] Determine if the connection comes from LAN, e.g. 192.168.0.0/16' -k, --key=[KEY] 'Only allow the client with the same key' - , --must-login=[Y|N] 'Only allow the client with login'", + , --must-login=[Y|N] 'Only allow the client with login (default Y)'", ); init_args(&args, "hbbs", "RustDesk ID/Rendezvous Server"); let port = get_arg_or("port", RENDEZVOUS_PORT.to_string()).parse::()?; diff --git a/src/rendezvous_server.rs b/src/rendezvous_server.rs index 4ddc613..a459c5f 100644 --- a/src/rendezvous_server.rs +++ b/src/rendezvous_server.rs @@ -40,6 +40,7 @@ use hbb_common::{ use ipnetwork::Ipv4Network; use crate::jwt; +use crate::token_device; use std::io::Error; use std::{ collections::HashMap, @@ -232,13 +233,16 @@ impl RendezvousServer { let must_login = get_arg("must-login"); log::debug!("must_login={}", must_login); - if must_login.to_uppercase() == "Y" + // 默认开启:仅当显式配置为 N(--must-login=N 或环境变量 MUST_LOGIN=N)时关闭 + if must_login.to_uppercase() == "N" || (must_login == "" && std::env::var("MUST_LOGIN") .unwrap_or_default() .to_uppercase() - == "Y") + == "N") { + MUST_LOGIN.store(false, Ordering::SeqCst); + } else { MUST_LOGIN.store(true, Ordering::SeqCst); } @@ -250,6 +254,11 @@ impl RendezvousServer { "N" } ); + if MUST_LOGIN.load(Ordering::SeqCst) && jwt::SECRET.is_empty() { + log::warn!("RUSTDESK_API_JWT_KEY is not set: client token verification and self-connection blocking are DISABLED (fail-open). Set RUSTDESK_API_JWT_KEY to enable them."); + } else if !jwt::SECRET.is_empty() { + log::info!("Self-connection blocking: enabled (requires client login)"); + } if test_addr.to_lowercase() != "no" { let test_addr = if test_addr.is_empty() { listener.local_addr()? @@ -562,6 +571,31 @@ impl RendezvousServer { if let Some(sink) = sink.take() { self.tcp_punch.lock().await.insert(try_into_v4(addr), sink); } + // 补上 MUST_LOGIN 强制校验(打洞处已有,中继处此前缺失) + if MUST_LOGIN.load(Ordering::SeqCst) + && (rf.token.is_empty() + || (!jwt::SECRET.is_empty() + && jwt::verify_token(&rf.token).is_err())) + { + log::warn!("RequestRelay rejected from {}: token missing or invalid", addr); + return true; + } + // 自连检测(fail-open:无 token/查询失败时跳过) + if !rf.token.is_empty() + && !jwt::SECRET.is_empty() + && jwt::verify_token(&rf.token).is_ok() + { + if let Some(device_id) = token_device::get_device_id(&rf.token).await { + if device_id == rf.id { + log::warn!( + "Blocked self-connection relay attempt from {} (device id {})", + addr, + rf.id + ); + return true; + } + } + } if let Some(peer) = self.pm.get_in_memory(&rf.id).await { let mut msg_out = RendezvousMessage::new(); rf.socket_addr = AddrMangle::encode(addr).into(); @@ -938,8 +972,7 @@ impl RendezvousServer { }); return Ok((msg_out, None)); } else if !jwt::SECRET.is_empty() { - let token = ph.token; - let token = jwt::verify_token(token.as_str()); + let token = jwt::verify_token(&ph.token); if token.is_err() { let mut msg_out = RendezvousMessage::new(); msg_out.set_punch_hole_response(PunchHoleResponse { @@ -951,6 +984,30 @@ impl RendezvousServer { } } } + // 自连检测:token 有效且查得到登录设备ID时,拦截"远程连接自己"。 + // 无 token(MUST_LOGIN=N 下未登录)或查询失败时跳过(fail-open,保持现状行为)。 + if !ph.token.is_empty() && !jwt::SECRET.is_empty() { + let token_ok = MUST_LOGIN.load(Ordering::SeqCst) || jwt::verify_token(&ph.token).is_ok(); + if token_ok { + if let Some(device_id) = token_device::get_device_id(&ph.token).await { + if device_id == ph.id { + log::warn!( + "Blocked self-connection attempt from {} (device id {})", + addr, + ph.id + ); + let mut msg_out = RendezvousMessage::new(); + msg_out.set_punch_hole_response(PunchHoleResponse { + other_failure: String::from( + "不能远程自己:发起方与目标是同一台设备 / You are connecting to yourself", + ), + ..Default::default() + }); + return Ok((msg_out, None)); + } + } + } + } let id = ph.id; // punch hole request from A, relay to B, // check if in same intranet first, diff --git a/src/token_device.rs b/src/token_device.rs new file mode 100644 index 0000000..b00d042 --- /dev/null +++ b/src/token_device.rs @@ -0,0 +1,100 @@ +// 查询 token 对应的登录设备ID(API 的 UserToken 表映射),用于自连拦截。 +// 查询结果在内存中缓存,命中可避免每次打洞都请求 API; +// API 不可达或未配置密钥时返回 None(fail-open,调用方跳过自连校验)。 +use hbb_common::log; +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +const CACHE_TTL: Duration = Duration::from_secs(300); +const NEGATIVE_TTL: Duration = Duration::from_secs(60); +const HTTP_TIMEOUT_SECS: u64 = 3; +const CACHE_CAP: usize = 4096; + +fn api_base() -> String { + std::env::var("RUSTDESK_API_INTERNAL_URL") + .unwrap_or_else(|_| "http://127.0.0.1:21114".to_owned()) +} + +struct CacheEntry { + device_id: Option, + at: Instant, +} + +static CACHE: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); + +fn query_api(token: &str) -> Option { + let secret = crate::jwt::SECRET.clone(); + if secret.is_empty() { + return None; + } + let url = format!( + "{}/api/internal/token-device?token={}", + api_base().trim_end_matches('/'), + token + ); + let resp = match minreq::get(&url) + .with_header("X-Secret", &secret) + .with_timeout(HTTP_TIMEOUT_SECS) + .send() + { + Ok(resp) => resp, + Err(err) => { + log::warn!("token-device query failed (fail-open): {}", err); + return None; + } + }; + if resp.status_code != 200 { + return None; + } + let body = resp.as_str().ok()?; + let v: serde_json::Value = serde_json::from_str(body).ok()?; + v.get("device_id")?.as_str().map(|s| s.to_owned()) +} + +async fn query_api_async(token: String) -> Option { + hbb_common::tokio::task::spawn_blocking(move || query_api(&token)) + .await + .unwrap_or(None) +} + +/// 返回 Some(device_id) 表示查到了登录设备ID; +/// None 表示无设备信息(fail-open,调用方跳过自连校验)。 +pub async fn get_device_id(token: &str) -> Option { + if token.is_empty() || crate::jwt::SECRET.is_empty() { + return None; + } + { + let cache = CACHE.lock().unwrap(); + if let Some(e) = cache.get(token) { + let ttl = if e.device_id.is_some() { + CACHE_TTL + } else { + NEGATIVE_TTL + }; + if e.at.elapsed() < ttl { + return e.device_id.clone(); + } + } + } + let device_id = query_api_async(token.to_owned()).await; + { + let mut cache = CACHE.lock().unwrap(); + if cache.len() >= CACHE_CAP { + cache.retain(|_, e| e.at.elapsed() < CACHE_TTL); + if cache.len() >= CACHE_CAP { + cache.clear(); + } + } + cache.insert( + token.to_owned(), + CacheEntry { + device_id: device_id.clone(), + at: Instant::now(), + }, + ); + } + device_id +}