feat: self-connection blocking (A connects to A) server-side solution
This commit is contained in:
1 parent
29a953bd92
commit
18a966c28e
5 files changed
+165
-7
No files matched your search
+1
-1
@@ -15,7 +15,7 @@ services:
|
||||
environment:
|
||||
- RELAY=<relay_server[:port]>
|
||||
- ENCRYPTED_ONLY=1
|
||||
- MUST_LOGIN=N
|
||||
- MUST_LOGIN=Y
|
||||
- TZ=Asia/Shanghai
|
||||
- RUSTDESK_API_RUSTDESK_ID_SERVER=<id_server[:21116]>
|
||||
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=<relay_server[:21117]>
|
||||
|
||||
+2
-1
@@ -4,4 +4,5 @@ pub mod common;
|
||||
mod database;
|
||||
mod peer;
|
||||
mod version;
|
||||
pub mod jwt;
|
||||
pub mod jwt;
|
||||
mod token_device;
|
||||
+1
-1
@@ -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::<i32>()?;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user