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:
|
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]>
|
||||||
|
|||||||
+2
-1
@@ -4,4 +4,5 @@ pub mod common;
|
|||||||
mod database;
|
mod database;
|
||||||
mod peer;
|
mod peer;
|
||||||
mod version;
|
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'
|
-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>()?;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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