multi_fix: harden peer/session handling, tighten foreign-network trust, and improve web client metadata (#1999)

* machine-id should be scoped unbder same user-id
* feat: report device os metadata to console
* fix sync root key cause packet loss
* fix tun packet not invalid
* fix faketcp cause lat jitter
* fix some packet not decrypt
* fix peer info patch, improve performance of update self info
* fix foreign credential identity mismatch handling
This commit is contained in:
KKRainbow
2026-03-21 21:06:07 +08:00
committed by GitHub
parent 77966916c4
commit 2bfdd44759
24 changed files with 1381 additions and 358 deletions
+7 -19
View File
@@ -175,27 +175,15 @@ impl ClientManager {
.map(|item| item.value().clone())
}
/// Find a session by machine_id regardless of user_id.
pub fn get_session_by_machine_id_global(
pub async fn disconnect_session_by_machine_id(
&self,
user_id: UserIdInDb,
machine_id: &uuid::Uuid,
) -> Option<Arc<Session>> {
self.storage
.get_client_url_by_machine_id_global(machine_id)
.and_then(|url| {
self.client_sessions
.get(&url)
.map(|item| item.value().clone())
})
}
/// Get user_id associated with a machine_id.
pub fn get_user_id_by_machine_id_global(&self, machine_id: &uuid::Uuid) -> Option<UserIdInDb> {
self.storage.get_user_id_by_machine_id_global(machine_id)
}
pub async fn disconnect_session_by_machine_id_global(&self, machine_id: &uuid::Uuid) -> bool {
let Some(client_url) = self.storage.get_client_url_by_machine_id_global(machine_id) else {
) -> bool {
let Some(client_url) = self
.storage
.get_client_url_by_machine_id(user_id, machine_id)
else {
return false;
};
let Some((_, session)) = self.client_sessions.remove(&client_url) else {
@@ -88,12 +88,16 @@ impl Drop for SessionData {
if self.webhook_config.is_enabled() {
let webhook = self.webhook_config.clone();
let machine_id = token.machine_id.to_string();
let user_id = Some(token.user_id);
let token_value = token.token.clone();
let web_instance_id = webhook.web_instance_id.clone();
let binding_version = self.binding_version;
tokio::spawn(async move {
webhook
.notify_node_disconnected(&crate::webhook::NodeDisconnectedRequest {
machine_id,
token: token_value,
user_id,
web_instance_id,
binding_version,
})
@@ -190,6 +194,9 @@ impl SessionRpcService {
machine_id: machine_id.to_string(),
hostname: req.hostname.clone(),
version: req.easytier_version.clone(),
os_type: req.device_os.as_ref().map(|info| info.os_type.clone()),
os_version: req.device_os.as_ref().map(|info| info.version.clone()),
os_distribution: req.device_os.as_ref().map(|info| info.distribution.clone()),
web_instance_id: data.webhook_config.web_instance_id.clone(),
web_instance_api_base_url: data.webhook_config.web_instance_api_base_url.clone(),
};
@@ -283,8 +290,12 @@ impl SessionRpcService {
let connect_req = crate::webhook::NodeConnectedRequest {
machine_id: machine_id.to_string(),
token: req.user_token.clone(),
user_id: Some(user_id),
hostname: req.hostname.clone(),
version: req.easytier_version.clone(),
os_type: req.device_os.as_ref().map(|info| info.os_type.clone()),
os_version: req.device_os.as_ref().map(|info| info.version.clone()),
os_distribution: req.device_os.as_ref().map(|info| info.distribution.clone()),
web_instance_id: webhook.web_instance_id.clone(),
binding_version,
};
+15 -39
View File
@@ -22,7 +22,6 @@ struct ClientInfo {
#[derive(Debug)]
pub struct StorageInner {
user_clients_map: DashMap<UserIdInDb, DashMap<uuid::Uuid, ClientInfo>>,
global_machine_map: DashMap<uuid::Uuid, ClientInfo>,
pub db: Db,
}
@@ -42,7 +41,6 @@ impl Storage {
pub fn new(db: Db) -> Self {
Storage(Arc::new(StorageInner {
user_clients_map: DashMap::new(),
global_machine_map: DashMap::new(),
db,
}))
}
@@ -75,13 +73,10 @@ impl Storage {
storage_token: stoken.clone(),
report_time,
};
Self::update_client_info_map(&inner, &client_info);
Self::update_client_info_map(&self.0.global_machine_map, &client_info);
}
pub fn remove_client(&self, stoken: &StorageToken) {
Self::remove_client_info_map(&self.0.global_machine_map, stoken);
self.0
.user_clients_map
.remove_if(&stoken.user_id, |_, set| {
@@ -106,22 +101,6 @@ impl Storage {
})
}
/// Find client_url by machine_id across all users.
pub fn get_client_url_by_machine_id_global(&self, machine_id: &uuid::Uuid) -> Option<url::Url> {
self.0
.global_machine_map
.get(machine_id)
.map(|info| info.storage_token.client_url.clone())
}
/// Find user_id by machine_id across all users.
pub fn get_user_id_by_machine_id_global(&self, machine_id: &uuid::Uuid) -> Option<UserIdInDb> {
self.0
.global_machine_map
.get(machine_id)
.map(|info| info.storage_token.user_id)
}
pub fn list_user_clients(&self, user_id: UserIdInDb) -> Vec<url::Url> {
self.0
.user_clients_map
@@ -164,38 +143,35 @@ mod tests {
}
#[tokio::test]
async fn global_machine_index_uses_latest_report_and_ignores_stale_removal() {
async fn machine_id_is_scoped_within_each_user() {
let storage = Storage::new(Db::memory_db().await);
let machine_id = uuid::Uuid::new_v4();
let old_token = make_storage_token(1, machine_id, "tcp://127.0.0.1:1001");
let new_token = make_storage_token(1, machine_id, "tcp://127.0.0.1:1002");
let user1_token = make_storage_token(1, machine_id, "tcp://127.0.0.1:1001");
let user2_token = make_storage_token(2, machine_id, "tcp://127.0.0.1:1002");
storage.update_client(old_token.clone(), 10);
storage.update_client(new_token.clone(), 20);
storage.update_client(user1_token.clone(), 10);
storage.update_client(user2_token.clone(), 20);
assert_eq!(
storage.get_client_url_by_machine_id_global(&machine_id),
Some(new_token.client_url.clone())
storage.get_client_url_by_machine_id(1, &machine_id),
Some(user1_token.client_url.clone())
);
assert_eq!(
storage.get_user_id_by_machine_id_global(&machine_id),
Some(1)
storage.get_client_url_by_machine_id(2, &machine_id),
Some(user2_token.client_url.clone())
);
storage.remove_client(&old_token);
storage.remove_client(&user1_token);
assert_eq!(storage.get_client_url_by_machine_id(1, &machine_id), None);
assert_eq!(
storage.get_client_url_by_machine_id_global(&machine_id),
Some(new_token.client_url.clone())
storage.get_client_url_by_machine_id(2, &machine_id),
Some(user2_token.client_url.clone())
);
storage.remove_client(&new_token);
storage.remove_client(&user2_token);
assert_eq!(
storage.get_client_url_by_machine_id_global(&machine_id),
None
);
assert_eq!(storage.get_user_id_by_machine_id_global(&machine_id), None);
assert_eq!(storage.get_client_url_by_machine_id(2, &machine_id), None);
}
}
+4 -4
View File
@@ -31,7 +31,7 @@ use users::{AuthSession, Backend};
use crate::client_manager::storage::StorageToken;
use crate::client_manager::ClientManager;
use crate::db::Db;
use crate::db::{Db, UserIdInDb};
use crate::webhook::SharedWebhookConfig;
use crate::FeatureFlags;
@@ -252,7 +252,7 @@ impl RestfulServer {
get(Self::handle_list_all_sessions_internal),
)
.route(
"/api/internal/sessions/:machine-id",
"/api/internal/users/:user-id/sessions/:machine-id",
delete(Self::handle_disconnect_session_internal),
)
.merge(NetworkApi::build_route_internal())
@@ -315,11 +315,11 @@ impl RestfulServer {
}
async fn handle_disconnect_session_internal(
Path(machine_id): Path<uuid::Uuid>,
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
State(client_mgr): AppState,
) -> Result<StatusCode, HttpHandleError> {
if client_mgr
.disconnect_session_by_machine_id_global(&machine_id)
.disconnect_session_by_machine_id(user_id, &machine_id)
.await
{
Ok(StatusCode::NO_CONTENT)
+7 -24
View File
@@ -299,10 +299,9 @@ impl NetworkApi {
async fn handle_run_network_instance_internal(
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
Json(payload): Json<RunNetworkJsonReq>,
) -> Result<Json<Void>, HttpHandleError> {
let user_id = Self::get_user_id_from_machine(&client_mgr, &machine_id)?;
client_mgr
.handle_run_network_instance((user_id, machine_id), payload.config, payload.save)
.await
@@ -312,9 +311,8 @@ impl NetworkApi {
async fn handle_remove_network_instance_internal(
State(client_mgr): AppState,
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
Path((user_id, machine_id, inst_id)): Path<(UserIdInDb, uuid::Uuid, uuid::Uuid)>,
) -> Result<(), HttpHandleError> {
let user_id = Self::get_user_id_from_machine(&client_mgr, &machine_id)?;
client_mgr
.handle_remove_network_instances((user_id, machine_id), vec![inst_id])
.await
@@ -323,9 +321,8 @@ impl NetworkApi {
async fn handle_list_network_instance_ids_internal(
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
) -> Result<Json<ListNetworkInstanceIdsJsonResp>, HttpHandleError> {
let user_id = Self::get_user_id_from_machine(&client_mgr, &machine_id)?;
Ok(client_mgr
.handle_list_network_instance_ids((user_id, machine_id))
.await
@@ -335,10 +332,9 @@ impl NetworkApi {
async fn handle_collect_network_info_internal(
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
Json(payload): Json<CollectNetworkInfoJsonReq>,
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
let user_id = Self::get_user_id_from_machine(&client_mgr, &machine_id)?;
Ok(client_mgr
.handle_collect_network_info((user_id, machine_id), payload.inst_ids)
.await
@@ -346,32 +342,19 @@ impl NetworkApi {
.into())
}
/// Look up user_id from a machine's active session token.
fn get_user_id_from_machine(
client_mgr: &AppStateInner,
machine_id: &uuid::Uuid,
) -> Result<UserIdInDb, HttpHandleError> {
client_mgr
.get_user_id_by_machine_id_global(machine_id)
.ok_or((
StatusCode::NOT_FOUND,
other_error("Machine not found").into(),
))
}
pub fn build_route_internal() -> Router<AppStateInner> {
Router::new()
.route(
"/api/internal/machines/:machine-id/networks",
"/api/internal/users/:user-id/machines/:machine-id/networks",
post(Self::handle_run_network_instance_internal)
.get(Self::handle_list_network_instance_ids_internal),
)
.route(
"/api/internal/machines/:machine-id/networks/:inst-id",
"/api/internal/users/:user-id/machines/:machine-id/networks/:inst-id",
delete(Self::handle_remove_network_instance_internal),
)
.route(
"/api/internal/machines/:machine-id/networks/info",
"/api/internal/users/:user-id/machines/:machine-id/networks/info",
get(Self::handle_collect_network_info_internal),
)
}
+5 -3
View File
@@ -7,6 +7,8 @@ use axum::{
use axum_login::AuthUser as _;
use easytier::proto::rpc_types::controller::BaseController;
use crate::db::UserIdInDb;
use super::{other_error, AppState, HttpHandleError};
#[derive(Debug, serde::Deserialize)]
@@ -162,11 +164,11 @@ pub fn router() -> Router<super::AppStateInner> {
/// Internal proxy-rpc handler: no AuthSession, resolves the active session by machine_id.
pub async fn handle_proxy_rpc_internal(
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
Json(req): Json<ProxyRpcRequest>,
) -> Result<Json<serde_json::Value>, HttpHandleError> {
let session = client_mgr
.get_session_by_machine_id_global(&machine_id)
.get_session_by_machine_id(user_id, &machine_id)
.ok_or((
StatusCode::NOT_FOUND,
other_error("Session not found").into(),
@@ -176,7 +178,7 @@ pub async fn handle_proxy_rpc_internal(
pub fn router_internal() -> Router<super::AppStateInner> {
Router::new().route(
"/api/internal/machines/:machine-id/proxy-rpc",
"/api/internal/users/:user-id/machines/:machine-id/proxy-rpc",
post(handle_proxy_rpc_internal),
)
}
+9
View File
@@ -51,6 +51,9 @@ pub struct ValidateTokenRequest {
pub machine_id: String,
pub hostname: String,
pub version: String,
pub os_type: Option<String>,
pub os_version: Option<String>,
pub os_distribution: Option<String>,
pub web_instance_id: Option<String>,
pub web_instance_api_base_url: Option<String>,
}
@@ -69,8 +72,12 @@ pub struct ValidateTokenResponse {
pub struct NodeConnectedRequest {
pub machine_id: String,
pub token: String,
pub user_id: Option<i32>,
pub hostname: String,
pub version: String,
pub os_type: Option<String>,
pub os_version: Option<String>,
pub os_distribution: Option<String>,
pub web_instance_id: Option<String>,
pub binding_version: Option<u64>,
}
@@ -78,6 +85,8 @@ pub struct NodeConnectedRequest {
#[derive(Debug, Serialize)]
pub struct NodeDisconnectedRequest {
pub machine_id: String,
pub token: String,
pub user_id: Option<i32>,
pub web_instance_id: Option<String>,
pub binding_version: Option<u64>,
}