feat: self-connection blocking (A connects to A) server-side solution

This commit is contained in:
admin committed 2026-09-16 13:33:22 +08:00
1 parent 29a953bd92
commit 18a966c28e
5 files changed
+164 -6

No files matched your search

+1 -1
View File
@@ -15,7 +15,7 @@ services:
environment: environment:
- RELAY=<relay_server[:port]> - RELAY=<relay_server[:port]>
- ENCRYPTED_ONLY=1 - ENCRYPTED_ONLY=1
- MUST_LOGIN=N - MUST_LOGIN=Y
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
- RUSTDESK_API_RUSTDESK_ID_SERVER=<id_server[:21116]> - RUSTDESK_API_RUSTDESK_ID_SERVER=<id_server[:21116]>
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=<relay_server[:21117]> - RUSTDESK_API_RUSTDESK_RELAY_SERVER=<relay_server[:21117]>
+1
View File
@@ -5,3 +5,4 @@ mod database;
mod peer; mod peer;
mod version; mod version;
pub mod jwt; pub mod jwt;
mod token_device;
+1 -1
View File
@@ -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' -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' , --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' -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"); init_args(&args, "hbbs", "RustDesk ID/Rendezvous Server");
let port = get_arg_or("port", RENDEZVOUS_PORT.to_string()).parse::<i32>()?; let port = get_arg_or("port", RENDEZVOUS_PORT.to_string()).parse::<i32>()?;
+61 -4
View File
@@ -40,6 +40,7 @@ use hbb_common::{
use ipnetwork::Ipv4Network; use ipnetwork::Ipv4Network;
use crate::jwt; use crate::jwt;
use crate::token_device;
use std::io::Error; use std::io::Error;
use std::{ use std::{
collections::HashMap, collections::HashMap,
@@ -232,13 +233,16 @@ impl RendezvousServer {
let must_login = get_arg("must-login"); let must_login = get_arg("must-login");
log::debug!("must_login={}", 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 == "" || (must_login == ""
&& std::env::var("MUST_LOGIN") && std::env::var("MUST_LOGIN")
.unwrap_or_default() .unwrap_or_default()
.to_uppercase() .to_uppercase()
== "Y") == "N")
{ {
MUST_LOGIN.store(false, Ordering::SeqCst);
} else {
MUST_LOGIN.store(true, Ordering::SeqCst); MUST_LOGIN.store(true, Ordering::SeqCst);
} }
@@ -250,6 +254,11 @@ impl RendezvousServer {
"N" "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" { if test_addr.to_lowercase() != "no" {
let test_addr = if test_addr.is_empty() { let test_addr = if test_addr.is_empty() {
listener.local_addr()? listener.local_addr()?
@@ -562,6 +571,31 @@ impl RendezvousServer {
if let Some(sink) = sink.take() { if let Some(sink) = sink.take() {
self.tcp_punch.lock().await.insert(try_into_v4(addr), sink); 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 { if let Some(peer) = self.pm.get_in_memory(&rf.id).await {
let mut msg_out = RendezvousMessage::new(); let mut msg_out = RendezvousMessage::new();
rf.socket_addr = AddrMangle::encode(addr).into(); rf.socket_addr = AddrMangle::encode(addr).into();
@@ -938,8 +972,7 @@ impl RendezvousServer {
}); });
return Ok((msg_out, None)); return Ok((msg_out, None));
} else if !jwt::SECRET.is_empty() { } else if !jwt::SECRET.is_empty() {
let token = ph.token; let token = jwt::verify_token(&ph.token);
let token = jwt::verify_token(token.as_str());
if token.is_err() { if token.is_err() {
let mut msg_out = RendezvousMessage::new(); let mut msg_out = RendezvousMessage::new();
msg_out.set_punch_hole_response(PunchHoleResponse { 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; let id = ph.id;
// punch hole request from A, relay to B, // punch hole request from A, relay to B,
// check if in same intranet first, // check if in same intranet first,
+100
View File
@@ -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<String>,
at: Instant,
}
static CACHE: Lazy<Mutex<HashMap<String, CacheEntry>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
fn query_api(token: &str) -> Option<String> {
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<String> {
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<String> {
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
}