mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-07 18:24:36 +00:00
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:
@@ -175,27 +175,15 @@ impl ClientManager {
|
|||||||
.map(|item| item.value().clone())
|
.map(|item| item.value().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a session by machine_id regardless of user_id.
|
pub async fn disconnect_session_by_machine_id(
|
||||||
pub fn get_session_by_machine_id_global(
|
|
||||||
&self,
|
&self,
|
||||||
|
user_id: UserIdInDb,
|
||||||
machine_id: &uuid::Uuid,
|
machine_id: &uuid::Uuid,
|
||||||
) -> Option<Arc<Session>> {
|
) -> bool {
|
||||||
self.storage
|
let Some(client_url) = self
|
||||||
.get_client_url_by_machine_id_global(machine_id)
|
.storage
|
||||||
.and_then(|url| {
|
.get_client_url_by_machine_id(user_id, machine_id)
|
||||||
self.client_sessions
|
else {
|
||||||
.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 {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let Some((_, session)) = self.client_sessions.remove(&client_url) else {
|
let Some((_, session)) = self.client_sessions.remove(&client_url) else {
|
||||||
|
|||||||
@@ -88,12 +88,16 @@ impl Drop for SessionData {
|
|||||||
if self.webhook_config.is_enabled() {
|
if self.webhook_config.is_enabled() {
|
||||||
let webhook = self.webhook_config.clone();
|
let webhook = self.webhook_config.clone();
|
||||||
let machine_id = token.machine_id.to_string();
|
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 web_instance_id = webhook.web_instance_id.clone();
|
||||||
let binding_version = self.binding_version;
|
let binding_version = self.binding_version;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
webhook
|
webhook
|
||||||
.notify_node_disconnected(&crate::webhook::NodeDisconnectedRequest {
|
.notify_node_disconnected(&crate::webhook::NodeDisconnectedRequest {
|
||||||
machine_id,
|
machine_id,
|
||||||
|
token: token_value,
|
||||||
|
user_id,
|
||||||
web_instance_id,
|
web_instance_id,
|
||||||
binding_version,
|
binding_version,
|
||||||
})
|
})
|
||||||
@@ -190,6 +194,9 @@ impl SessionRpcService {
|
|||||||
machine_id: machine_id.to_string(),
|
machine_id: machine_id.to_string(),
|
||||||
hostname: req.hostname.clone(),
|
hostname: req.hostname.clone(),
|
||||||
version: req.easytier_version.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_id: data.webhook_config.web_instance_id.clone(),
|
||||||
web_instance_api_base_url: data.webhook_config.web_instance_api_base_url.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 {
|
let connect_req = crate::webhook::NodeConnectedRequest {
|
||||||
machine_id: machine_id.to_string(),
|
machine_id: machine_id.to_string(),
|
||||||
token: req.user_token.clone(),
|
token: req.user_token.clone(),
|
||||||
|
user_id: Some(user_id),
|
||||||
hostname: req.hostname.clone(),
|
hostname: req.hostname.clone(),
|
||||||
version: req.easytier_version.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(),
|
web_instance_id: webhook.web_instance_id.clone(),
|
||||||
binding_version,
|
binding_version,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ struct ClientInfo {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StorageInner {
|
pub struct StorageInner {
|
||||||
user_clients_map: DashMap<UserIdInDb, DashMap<uuid::Uuid, ClientInfo>>,
|
user_clients_map: DashMap<UserIdInDb, DashMap<uuid::Uuid, ClientInfo>>,
|
||||||
global_machine_map: DashMap<uuid::Uuid, ClientInfo>,
|
|
||||||
pub db: Db,
|
pub db: Db,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +41,6 @@ impl Storage {
|
|||||||
pub fn new(db: Db) -> Self {
|
pub fn new(db: Db) -> Self {
|
||||||
Storage(Arc::new(StorageInner {
|
Storage(Arc::new(StorageInner {
|
||||||
user_clients_map: DashMap::new(),
|
user_clients_map: DashMap::new(),
|
||||||
global_machine_map: DashMap::new(),
|
|
||||||
db,
|
db,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -75,13 +73,10 @@ impl Storage {
|
|||||||
storage_token: stoken.clone(),
|
storage_token: stoken.clone(),
|
||||||
report_time,
|
report_time,
|
||||||
};
|
};
|
||||||
|
|
||||||
Self::update_client_info_map(&inner, &client_info);
|
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) {
|
pub fn remove_client(&self, stoken: &StorageToken) {
|
||||||
Self::remove_client_info_map(&self.0.global_machine_map, stoken);
|
|
||||||
self.0
|
self.0
|
||||||
.user_clients_map
|
.user_clients_map
|
||||||
.remove_if(&stoken.user_id, |_, set| {
|
.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> {
|
pub fn list_user_clients(&self, user_id: UserIdInDb) -> Vec<url::Url> {
|
||||||
self.0
|
self.0
|
||||||
.user_clients_map
|
.user_clients_map
|
||||||
@@ -164,38 +143,35 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 storage = Storage::new(Db::memory_db().await);
|
||||||
let machine_id = uuid::Uuid::new_v4();
|
let machine_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
let old_token = make_storage_token(1, machine_id, "tcp://127.0.0.1:1001");
|
let user1_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 user2_token = make_storage_token(2, machine_id, "tcp://127.0.0.1:1002");
|
||||||
|
|
||||||
storage.update_client(old_token.clone(), 10);
|
storage.update_client(user1_token.clone(), 10);
|
||||||
storage.update_client(new_token.clone(), 20);
|
storage.update_client(user2_token.clone(), 20);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
storage.get_client_url_by_machine_id_global(&machine_id),
|
storage.get_client_url_by_machine_id(1, &machine_id),
|
||||||
Some(new_token.client_url.clone())
|
Some(user1_token.client_url.clone())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
storage.get_user_id_by_machine_id_global(&machine_id),
|
storage.get_client_url_by_machine_id(2, &machine_id),
|
||||||
Some(1)
|
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!(
|
assert_eq!(
|
||||||
storage.get_client_url_by_machine_id_global(&machine_id),
|
storage.get_client_url_by_machine_id(2, &machine_id),
|
||||||
Some(new_token.client_url.clone())
|
Some(user2_token.client_url.clone())
|
||||||
);
|
);
|
||||||
|
|
||||||
storage.remove_client(&new_token);
|
storage.remove_client(&user2_token);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(storage.get_client_url_by_machine_id(2, &machine_id), None);
|
||||||
storage.get_client_url_by_machine_id_global(&machine_id),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
assert_eq!(storage.get_user_id_by_machine_id_global(&machine_id), None);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ use users::{AuthSession, Backend};
|
|||||||
|
|
||||||
use crate::client_manager::storage::StorageToken;
|
use crate::client_manager::storage::StorageToken;
|
||||||
use crate::client_manager::ClientManager;
|
use crate::client_manager::ClientManager;
|
||||||
use crate::db::Db;
|
use crate::db::{Db, UserIdInDb};
|
||||||
use crate::webhook::SharedWebhookConfig;
|
use crate::webhook::SharedWebhookConfig;
|
||||||
use crate::FeatureFlags;
|
use crate::FeatureFlags;
|
||||||
|
|
||||||
@@ -252,7 +252,7 @@ impl RestfulServer {
|
|||||||
get(Self::handle_list_all_sessions_internal),
|
get(Self::handle_list_all_sessions_internal),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/internal/sessions/:machine-id",
|
"/api/internal/users/:user-id/sessions/:machine-id",
|
||||||
delete(Self::handle_disconnect_session_internal),
|
delete(Self::handle_disconnect_session_internal),
|
||||||
)
|
)
|
||||||
.merge(NetworkApi::build_route_internal())
|
.merge(NetworkApi::build_route_internal())
|
||||||
@@ -315,11 +315,11 @@ impl RestfulServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_disconnect_session_internal(
|
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,
|
State(client_mgr): AppState,
|
||||||
) -> Result<StatusCode, HttpHandleError> {
|
) -> Result<StatusCode, HttpHandleError> {
|
||||||
if client_mgr
|
if client_mgr
|
||||||
.disconnect_session_by_machine_id_global(&machine_id)
|
.disconnect_session_by_machine_id(user_id, &machine_id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(StatusCode::NO_CONTENT)
|
Ok(StatusCode::NO_CONTENT)
|
||||||
|
|||||||
@@ -299,10 +299,9 @@ impl NetworkApi {
|
|||||||
|
|
||||||
async fn handle_run_network_instance_internal(
|
async fn handle_run_network_instance_internal(
|
||||||
State(client_mgr): AppState,
|
State(client_mgr): AppState,
|
||||||
Path(machine_id): Path<uuid::Uuid>,
|
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
|
||||||
Json(payload): Json<RunNetworkJsonReq>,
|
Json(payload): Json<RunNetworkJsonReq>,
|
||||||
) -> Result<Json<Void>, HttpHandleError> {
|
) -> Result<Json<Void>, HttpHandleError> {
|
||||||
let user_id = Self::get_user_id_from_machine(&client_mgr, &machine_id)?;
|
|
||||||
client_mgr
|
client_mgr
|
||||||
.handle_run_network_instance((user_id, machine_id), payload.config, payload.save)
|
.handle_run_network_instance((user_id, machine_id), payload.config, payload.save)
|
||||||
.await
|
.await
|
||||||
@@ -312,9 +311,8 @@ impl NetworkApi {
|
|||||||
|
|
||||||
async fn handle_remove_network_instance_internal(
|
async fn handle_remove_network_instance_internal(
|
||||||
State(client_mgr): AppState,
|
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> {
|
) -> Result<(), HttpHandleError> {
|
||||||
let user_id = Self::get_user_id_from_machine(&client_mgr, &machine_id)?;
|
|
||||||
client_mgr
|
client_mgr
|
||||||
.handle_remove_network_instances((user_id, machine_id), vec![inst_id])
|
.handle_remove_network_instances((user_id, machine_id), vec![inst_id])
|
||||||
.await
|
.await
|
||||||
@@ -323,9 +321,8 @@ impl NetworkApi {
|
|||||||
|
|
||||||
async fn handle_list_network_instance_ids_internal(
|
async fn handle_list_network_instance_ids_internal(
|
||||||
State(client_mgr): AppState,
|
State(client_mgr): AppState,
|
||||||
Path(machine_id): Path<uuid::Uuid>,
|
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
|
||||||
) -> Result<Json<ListNetworkInstanceIdsJsonResp>, HttpHandleError> {
|
) -> Result<Json<ListNetworkInstanceIdsJsonResp>, HttpHandleError> {
|
||||||
let user_id = Self::get_user_id_from_machine(&client_mgr, &machine_id)?;
|
|
||||||
Ok(client_mgr
|
Ok(client_mgr
|
||||||
.handle_list_network_instance_ids((user_id, machine_id))
|
.handle_list_network_instance_ids((user_id, machine_id))
|
||||||
.await
|
.await
|
||||||
@@ -335,10 +332,9 @@ impl NetworkApi {
|
|||||||
|
|
||||||
async fn handle_collect_network_info_internal(
|
async fn handle_collect_network_info_internal(
|
||||||
State(client_mgr): AppState,
|
State(client_mgr): AppState,
|
||||||
Path(machine_id): Path<uuid::Uuid>,
|
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
|
||||||
Json(payload): Json<CollectNetworkInfoJsonReq>,
|
Json(payload): Json<CollectNetworkInfoJsonReq>,
|
||||||
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
|
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
|
||||||
let user_id = Self::get_user_id_from_machine(&client_mgr, &machine_id)?;
|
|
||||||
Ok(client_mgr
|
Ok(client_mgr
|
||||||
.handle_collect_network_info((user_id, machine_id), payload.inst_ids)
|
.handle_collect_network_info((user_id, machine_id), payload.inst_ids)
|
||||||
.await
|
.await
|
||||||
@@ -346,32 +342,19 @@ impl NetworkApi {
|
|||||||
.into())
|
.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> {
|
pub fn build_route_internal() -> Router<AppStateInner> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/api/internal/machines/:machine-id/networks",
|
"/api/internal/users/:user-id/machines/:machine-id/networks",
|
||||||
post(Self::handle_run_network_instance_internal)
|
post(Self::handle_run_network_instance_internal)
|
||||||
.get(Self::handle_list_network_instance_ids_internal),
|
.get(Self::handle_list_network_instance_ids_internal),
|
||||||
)
|
)
|
||||||
.route(
|
.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),
|
delete(Self::handle_remove_network_instance_internal),
|
||||||
)
|
)
|
||||||
.route(
|
.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),
|
get(Self::handle_collect_network_info_internal),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ use axum::{
|
|||||||
use axum_login::AuthUser as _;
|
use axum_login::AuthUser as _;
|
||||||
use easytier::proto::rpc_types::controller::BaseController;
|
use easytier::proto::rpc_types::controller::BaseController;
|
||||||
|
|
||||||
|
use crate::db::UserIdInDb;
|
||||||
|
|
||||||
use super::{other_error, AppState, HttpHandleError};
|
use super::{other_error, AppState, HttpHandleError};
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[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.
|
/// Internal proxy-rpc handler: no AuthSession, resolves the active session by machine_id.
|
||||||
pub async fn handle_proxy_rpc_internal(
|
pub async fn handle_proxy_rpc_internal(
|
||||||
State(client_mgr): AppState,
|
State(client_mgr): AppState,
|
||||||
Path(machine_id): Path<uuid::Uuid>,
|
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
|
||||||
Json(req): Json<ProxyRpcRequest>,
|
Json(req): Json<ProxyRpcRequest>,
|
||||||
) -> Result<Json<serde_json::Value>, HttpHandleError> {
|
) -> Result<Json<serde_json::Value>, HttpHandleError> {
|
||||||
let session = client_mgr
|
let session = client_mgr
|
||||||
.get_session_by_machine_id_global(&machine_id)
|
.get_session_by_machine_id(user_id, &machine_id)
|
||||||
.ok_or((
|
.ok_or((
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
other_error("Session not found").into(),
|
other_error("Session not found").into(),
|
||||||
@@ -176,7 +178,7 @@ pub async fn handle_proxy_rpc_internal(
|
|||||||
|
|
||||||
pub fn router_internal() -> Router<super::AppStateInner> {
|
pub fn router_internal() -> Router<super::AppStateInner> {
|
||||||
Router::new().route(
|
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),
|
post(handle_proxy_rpc_internal),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ pub struct ValidateTokenRequest {
|
|||||||
pub machine_id: String,
|
pub machine_id: String,
|
||||||
pub hostname: String,
|
pub hostname: String,
|
||||||
pub version: 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_id: Option<String>,
|
||||||
pub web_instance_api_base_url: Option<String>,
|
pub web_instance_api_base_url: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -69,8 +72,12 @@ pub struct ValidateTokenResponse {
|
|||||||
pub struct NodeConnectedRequest {
|
pub struct NodeConnectedRequest {
|
||||||
pub machine_id: String,
|
pub machine_id: String,
|
||||||
pub token: String,
|
pub token: String,
|
||||||
|
pub user_id: Option<i32>,
|
||||||
pub hostname: String,
|
pub hostname: String,
|
||||||
pub version: 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_id: Option<String>,
|
||||||
pub binding_version: Option<u64>,
|
pub binding_version: Option<u64>,
|
||||||
}
|
}
|
||||||
@@ -78,6 +85,8 @@ pub struct NodeConnectedRequest {
|
|||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct NodeDisconnectedRequest {
|
pub struct NodeDisconnectedRequest {
|
||||||
pub machine_id: String,
|
pub machine_id: String,
|
||||||
|
pub token: String,
|
||||||
|
pub user_id: Option<i32>,
|
||||||
pub web_instance_id: Option<String>,
|
pub web_instance_id: Option<String>,
|
||||||
pub binding_version: Option<u64>,
|
pub binding_version: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,15 @@ impl TrustedKeyMapManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify_trusted_key(&self, pubkey: &[u8], network_name: &str) -> bool {
|
pub fn verify_trusted_key(&self, pubkey: &[u8], network_name: &str) -> bool {
|
||||||
|
self.verify_trusted_key_with_source(pubkey, network_name, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_trusted_key_with_source(
|
||||||
|
&self,
|
||||||
|
pubkey: &[u8],
|
||||||
|
network_name: &str,
|
||||||
|
source: Option<TrustedKeySource>,
|
||||||
|
) -> bool {
|
||||||
let Some(trusted_keys) = self
|
let Some(trusted_keys) = self
|
||||||
.network_trusted_keys
|
.network_trusted_keys
|
||||||
.get(network_name)
|
.get(network_name)
|
||||||
@@ -146,8 +155,12 @@ impl TrustedKeyMapManager {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(source) = source {
|
||||||
|
metadata.source == source && !metadata.is_expired()
|
||||||
|
} else {
|
||||||
!metadata.is_expired()
|
!metadata.is_expired()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn list_trusted_keys(&self, network_name: &str) -> Vec<(Vec<u8>, TrustedKeyMetadata)> {
|
pub fn list_trusted_keys(&self, network_name: &str) -> Vec<(Vec<u8>, TrustedKeyMetadata)> {
|
||||||
let Some(trusted_keys) = self
|
let Some(trusted_keys) = self
|
||||||
@@ -542,6 +555,16 @@ impl GlobalCtx {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_pubkey_trusted_with_source(
|
||||||
|
&self,
|
||||||
|
pubkey: &[u8],
|
||||||
|
network_name: &str,
|
||||||
|
source: TrustedKeySource,
|
||||||
|
) -> bool {
|
||||||
|
self.trusted_keys
|
||||||
|
.verify_trusted_key_with_source(pubkey, network_name, Some(source))
|
||||||
|
}
|
||||||
|
|
||||||
/// Atomically replace all OSPF trusted keys with a new set
|
/// Atomically replace all OSPF trusted keys with a new set
|
||||||
/// Called by OSPF route layer after each route update
|
/// Called by OSPF route layer after each route update
|
||||||
pub fn update_trusted_keys(&self, keys: TrustedKeyMap, network_name: &str) {
|
pub fn update_trusted_keys(&self, keys: TrustedKeyMap, network_name: &str) {
|
||||||
@@ -676,6 +699,37 @@ pub mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn trusted_key_source_lookup_is_precise() {
|
||||||
|
let config = TomlConfigLoader::default();
|
||||||
|
let global_ctx = GlobalCtx::new(config);
|
||||||
|
let network_name = "net1";
|
||||||
|
let pubkey = vec![1; 32];
|
||||||
|
|
||||||
|
global_ctx.update_trusted_keys(
|
||||||
|
HashMap::from([(
|
||||||
|
pubkey.clone(),
|
||||||
|
TrustedKeyMetadata {
|
||||||
|
source: TrustedKeySource::OspfCredential,
|
||||||
|
expiry_unix: None,
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
network_name,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(global_ctx.is_pubkey_trusted(&pubkey, network_name));
|
||||||
|
assert!(!global_ctx.is_pubkey_trusted_with_source(
|
||||||
|
&pubkey,
|
||||||
|
network_name,
|
||||||
|
TrustedKeySource::OspfNode,
|
||||||
|
));
|
||||||
|
assert!(global_ctx.is_pubkey_trusted_with_source(
|
||||||
|
&pubkey,
|
||||||
|
network_name,
|
||||||
|
TrustedKeySource::OspfCredential,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_mock_global_ctx_with_network(
|
pub fn get_mock_global_ctx_with_network(
|
||||||
network_identy: Option<NetworkIdentity>,
|
network_identy: Option<NetworkIdentity>,
|
||||||
) -> ArcGlobalCtx {
|
) -> ArcGlobalCtx {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub mod ifcfg;
|
|||||||
pub mod log;
|
pub mod log;
|
||||||
pub mod netns;
|
pub mod netns;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
pub mod os_info;
|
||||||
pub mod scoped_task;
|
pub mod scoped_task;
|
||||||
pub mod stats_manager;
|
pub mod stats_manager;
|
||||||
pub mod stun;
|
pub mod stun;
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
use std::{collections::HashMap, fs, process::Command};
|
||||||
|
|
||||||
|
use crate::proto::web::DeviceOsInfo;
|
||||||
|
|
||||||
|
pub fn collect_device_os_info() -> DeviceOsInfo {
|
||||||
|
let os_type = normalize_os_type(std::env::consts::OS);
|
||||||
|
let (version, distribution) = detect_os_version_and_distribution(&os_type);
|
||||||
|
|
||||||
|
DeviceOsInfo {
|
||||||
|
os_type,
|
||||||
|
version,
|
||||||
|
distribution,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_os_type(raw: &str) -> String {
|
||||||
|
match raw {
|
||||||
|
"macos" => "macos".to_string(),
|
||||||
|
"windows" => "windows".to_string(),
|
||||||
|
"linux" => "linux".to_string(),
|
||||||
|
"android" => "android".to_string(),
|
||||||
|
"ios" => "ios".to_string(),
|
||||||
|
"freebsd" => "freebsd".to_string(),
|
||||||
|
other => other.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_os_version_and_distribution(os_type: &str) -> (String, String) {
|
||||||
|
match os_type {
|
||||||
|
"linux" | "android" => linux_version_and_distribution(os_type),
|
||||||
|
"macos" => (
|
||||||
|
first_non_empty([
|
||||||
|
command_output("sw_vers", &["-productVersion"]),
|
||||||
|
unix_kernel_release(),
|
||||||
|
]),
|
||||||
|
"macOS".to_string(),
|
||||||
|
),
|
||||||
|
"windows" => (
|
||||||
|
first_non_empty([windows_version(), None]),
|
||||||
|
"Windows".to_string(),
|
||||||
|
),
|
||||||
|
"freebsd" => (
|
||||||
|
first_non_empty([
|
||||||
|
command_output("freebsd-version", &[]),
|
||||||
|
unix_kernel_release(),
|
||||||
|
]),
|
||||||
|
"FreeBSD".to_string(),
|
||||||
|
),
|
||||||
|
other => (
|
||||||
|
unix_kernel_release().unwrap_or_else(|| "unknown".to_string()),
|
||||||
|
other.to_string(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn linux_version_and_distribution(os_type: &str) -> (String, String) {
|
||||||
|
let os_release = parse_os_release().unwrap_or_default();
|
||||||
|
let version = first_non_empty([
|
||||||
|
os_release.get("VERSION_ID").cloned(),
|
||||||
|
os_release.get("VERSION").cloned(),
|
||||||
|
unix_kernel_release(),
|
||||||
|
]);
|
||||||
|
let distribution = first_non_empty([
|
||||||
|
os_release.get("NAME").cloned(),
|
||||||
|
os_release.get("ID").cloned().map(title_case),
|
||||||
|
Some(if os_type == "android" {
|
||||||
|
"Android".to_string()
|
||||||
|
} else {
|
||||||
|
"Linux".to_string()
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
(version, distribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_os_release() -> Option<HashMap<String, String>> {
|
||||||
|
["/etc/os-release", "/usr/lib/os-release"]
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|path| fs::read_to_string(path).ok())
|
||||||
|
.map(|content| {
|
||||||
|
content
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (key, value) = line.split_once('=')?;
|
||||||
|
Some((key.to_string(), trim_os_release_value(value)))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trim_os_release_value(value: &str) -> String {
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.trim_matches('"')
|
||||||
|
.trim_matches('\'')
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unix_kernel_release() -> Option<String> {
|
||||||
|
command_output("uname", &["-r"])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn windows_version() -> Option<String> {
|
||||||
|
let output = command_output("cmd", &["/C", "ver"])?;
|
||||||
|
output
|
||||||
|
.split("Version")
|
||||||
|
.nth(1)
|
||||||
|
.map(str::trim)
|
||||||
|
.map(|part| part.trim_matches(&['[', ']'][..]).to_string())
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_output(program: &str, args: &[&str]) -> Option<String> {
|
||||||
|
let output = Command::new(program).args(args).output().ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let value = String::from_utf8(output.stdout).ok()?;
|
||||||
|
let value = value.trim();
|
||||||
|
if value.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_non_empty<const N: usize>(values: [Option<String>; N]) -> String {
|
||||||
|
values
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.find(|value| !value.trim().is_empty())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title_case(value: String) -> String {
|
||||||
|
let mut chars = value.chars();
|
||||||
|
let Some(first) = chars.next() else {
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
first.to_uppercase().collect::<String>() + chars.as_str()
|
||||||
|
}
|
||||||
@@ -303,6 +303,26 @@ impl ForeignNetworkEntry {
|
|||||||
fn my_peer_id(&self) -> PeerId {
|
fn my_peer_id(&self) -> PeerId {
|
||||||
self.my_peer_id
|
self.my_peer_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn need_periodic_requery_peers(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_peer_identity_type(&self, peer_id: PeerId) -> Option<PeerIdentityType> {
|
||||||
|
let peer_map = self.peer_map.upgrade()?;
|
||||||
|
peer_map.get_peer_identity_type(peer_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_peer_public_key(&self, peer_id: PeerId) -> Option<Vec<u8>> {
|
||||||
|
let peer_map = self.peer_map.upgrade()?;
|
||||||
|
peer_map.get_peer_public_key(peer_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close_peer(&self, peer_id: PeerId) {
|
||||||
|
if let Some(peer_map) = self.peer_map.upgrade() {
|
||||||
|
let _ = peer_map.close_peer(peer_id).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let route = PeerRoute::new(
|
let route = PeerRoute::new(
|
||||||
@@ -373,15 +393,15 @@ impl ForeignNetworkEntry {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !peer_map.has_peer(from_peer_id) && relay_peer_map.is_secure_mode_enabled() {
|
if relay_peer_map.is_secure_mode_enabled() && hdr.is_encrypted() {
|
||||||
match relay_peer_map.decrypt_if_needed(&mut zc_packet).await {
|
match relay_peer_map.decrypt_if_needed(&mut zc_packet).await {
|
||||||
Ok(true) => {}
|
Ok(true) => {}
|
||||||
Ok(false) => {
|
Ok(false) => {
|
||||||
tracing::error!("relay session not found");
|
tracing::error!("secure session not found");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(?e, "relay decrypt failed");
|
tracing::error!(?e, "secure decrypt failed");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -620,14 +640,27 @@ pub struct ForeignNetworkManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ForeignNetworkManager {
|
impl ForeignNetworkManager {
|
||||||
async fn is_shared_pubkey_trusted(
|
fn network_secret_digest_is_empty(network: &NetworkIdentity) -> bool {
|
||||||
|
network
|
||||||
|
.network_secret_digest
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|d| d.iter().all(|b| *b == 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_reject_credential_trust_path(identity_type: PeerIdentityType) -> bool {
|
||||||
|
matches!(identity_type, PeerIdentityType::Admin)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_credential_pubkey_trusted(
|
||||||
entry: &ForeignNetworkEntry,
|
entry: &ForeignNetworkEntry,
|
||||||
remote_static_pubkey: &[u8],
|
remote_static_pubkey: &[u8],
|
||||||
) -> bool {
|
) -> bool {
|
||||||
remote_static_pubkey.len() == 32
|
remote_static_pubkey.len() == 32
|
||||||
&& entry
|
&& entry.global_ctx.is_pubkey_trusted_with_source(
|
||||||
.global_ctx
|
remote_static_pubkey,
|
||||||
.is_pubkey_trusted(remote_static_pubkey, &entry.network.network_name)
|
&entry.network.network_name,
|
||||||
|
TrustedKeySource::OspfCredential,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_trusted_key_items(entry: &ForeignNetworkEntry) -> Vec<TrustedKeyInfoPb> {
|
fn build_trusted_key_items(entry: &ForeignNetworkEntry) -> Vec<TrustedKeyInfoPb> {
|
||||||
@@ -697,6 +730,20 @@ impl ForeignNetworkManager {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let peer_digest_empty = Self::network_secret_digest_is_empty(&peer_network);
|
||||||
|
if peer_digest_empty
|
||||||
|
&& self
|
||||||
|
.data
|
||||||
|
.get_network_entry(&peer_network.network_name)
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"foreign network {} is not established by a secret-verified peer yet",
|
||||||
|
peer_network.network_name
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
let (entry, new_added) = self
|
let (entry, new_added) = self
|
||||||
.data
|
.data
|
||||||
.get_or_insert_entry(
|
.get_or_insert_entry(
|
||||||
@@ -711,13 +758,17 @@ impl ForeignNetworkManager {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let same_identity = entry.network == peer_network;
|
let same_identity = entry.network == peer_network;
|
||||||
let shared_peer = peer_conn.get_peer_identity_type() == PeerIdentityType::SharedNode;
|
let peer_identity_type = peer_conn.get_peer_identity_type();
|
||||||
let shared_peer_trusted = shared_peer
|
let credential_peer_trusted = peer_digest_empty
|
||||||
&& Self::is_shared_pubkey_trusted(&entry, &conn_info.noise_remote_static_pubkey).await;
|
&& Self::is_credential_pubkey_trusted(&entry, &conn_info.noise_remote_static_pubkey)
|
||||||
|
.await;
|
||||||
|
let credential_identity_mismatch = credential_peer_trusted
|
||||||
|
&& Self::should_reject_credential_trust_path(peer_identity_type);
|
||||||
|
|
||||||
let _g = entry.lock.lock().await;
|
let _g = entry.lock.lock().await;
|
||||||
|
|
||||||
if (!(same_identity || shared_peer_trusted))
|
if (!(same_identity || credential_peer_trusted))
|
||||||
|
|| credential_identity_mismatch
|
||||||
|| entry.my_peer_id != peer_conn.get_my_peer_id()
|
|| entry.my_peer_id != peer_conn.get_my_peer_id()
|
||||||
{
|
{
|
||||||
if new_added {
|
if new_added {
|
||||||
@@ -730,13 +781,18 @@ impl ForeignNetworkManager {
|
|||||||
entry.my_peer_id,
|
entry.my_peer_id,
|
||||||
peer_conn.get_my_peer_id()
|
peer_conn.get_my_peer_id()
|
||||||
)
|
)
|
||||||
|
} else if credential_identity_mismatch {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"credential-trusted foreign peer has invalid identity type: {:?}",
|
||||||
|
peer_identity_type
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
anyhow::anyhow!(
|
anyhow::anyhow!(
|
||||||
"foreign peer identity not trusted. exp: {:?} real: {:?}, remote_pubkey_len: {}, shared_trusted: {}",
|
"foreign peer identity not trusted. exp: {:?} real: {:?}, remote_pubkey_len: {}, credential_trusted: {}",
|
||||||
entry.network,
|
entry.network,
|
||||||
peer_network,
|
peer_network,
|
||||||
conn_info.noise_remote_static_pubkey.len(),
|
conn_info.noise_remote_static_pubkey.len(),
|
||||||
shared_peer_trusted,
|
credential_peer_trusted,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
tracing::error!(?err, "foreign network entry not match, disconnect peer");
|
tracing::error!(?err, "foreign network entry not match, disconnect peer");
|
||||||
@@ -911,7 +967,7 @@ pub mod tests {
|
|||||||
set_global_var,
|
set_global_var,
|
||||||
tunnel::common::tests::wait_for_condition,
|
tunnel::common::tests::wait_for_condition,
|
||||||
};
|
};
|
||||||
use std::time::Duration;
|
use std::{collections::HashMap, time::Duration};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -933,6 +989,20 @@ pub mod tests {
|
|||||||
peer_mgr
|
peer_mgr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_mock_credential_peer_manager_for_foreign_network(
|
||||||
|
network: &str,
|
||||||
|
) -> Arc<PeerManager> {
|
||||||
|
let (s, _r) = create_packet_recv_chan();
|
||||||
|
let global_ctx = get_mock_global_ctx_with_network(Some(NetworkIdentity::new_credential(
|
||||||
|
network.to_string(),
|
||||||
|
)));
|
||||||
|
set_secure_mode_cfg(&global_ctx, true);
|
||||||
|
let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, global_ctx, s));
|
||||||
|
replace_stun_info_collector(peer_mgr.clone(), NatType::Unknown);
|
||||||
|
peer_mgr.run().await.unwrap();
|
||||||
|
peer_mgr
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_mock_peer_manager_for_foreign_network(network: &str) -> Arc<PeerManager> {
|
pub async fn create_mock_peer_manager_for_foreign_network(network: &str) -> Arc<PeerManager> {
|
||||||
create_mock_peer_manager_for_foreign_network_ext(network, network).await
|
create_mock_peer_manager_for_foreign_network_ext(network, network).await
|
||||||
}
|
}
|
||||||
@@ -1027,6 +1097,86 @@ pub mod tests {
|
|||||||
.is_empty());
|
.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn credential_pubkey_trust_requires_ospf_credential_source() {
|
||||||
|
let global_ctx = get_mock_global_ctx_with_network(Some(NetworkIdentity::new(
|
||||||
|
"__access__".to_string(),
|
||||||
|
"access_secret".to_string(),
|
||||||
|
)));
|
||||||
|
let foreign_network = NetworkIdentity::new("net1".to_string(), "net1_secret".to_string());
|
||||||
|
let (pm_packet_sender, _pm_packet_recv) = create_packet_recv_chan();
|
||||||
|
let entry = ForeignNetworkEntry::new(
|
||||||
|
foreign_network.clone(),
|
||||||
|
1,
|
||||||
|
global_ctx.clone(),
|
||||||
|
false,
|
||||||
|
Arc::new(PeerSessionStore::new()),
|
||||||
|
pm_packet_sender,
|
||||||
|
);
|
||||||
|
let pubkey = vec![7; 32];
|
||||||
|
|
||||||
|
entry.global_ctx.update_trusted_keys(
|
||||||
|
HashMap::from([(
|
||||||
|
pubkey.clone(),
|
||||||
|
crate::common::global_ctx::TrustedKeyMetadata {
|
||||||
|
source: TrustedKeySource::OspfNode,
|
||||||
|
expiry_unix: None,
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
&foreign_network.network_name,
|
||||||
|
);
|
||||||
|
assert!(!ForeignNetworkManager::is_credential_pubkey_trusted(&entry, &pubkey).await);
|
||||||
|
|
||||||
|
entry.global_ctx.update_trusted_keys(
|
||||||
|
HashMap::from([(
|
||||||
|
pubkey.clone(),
|
||||||
|
crate::common::global_ctx::TrustedKeyMetadata {
|
||||||
|
source: TrustedKeySource::OspfCredential,
|
||||||
|
expiry_unix: None,
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
&foreign_network.network_name,
|
||||||
|
);
|
||||||
|
assert!(ForeignNetworkManager::is_credential_pubkey_trusted(&entry, &pubkey).await);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn credential_trust_path_rejects_admin_identity() {
|
||||||
|
assert!(ForeignNetworkManager::should_reject_credential_trust_path(
|
||||||
|
PeerIdentityType::Admin
|
||||||
|
));
|
||||||
|
assert!(!ForeignNetworkManager::should_reject_credential_trust_path(
|
||||||
|
PeerIdentityType::Credential
|
||||||
|
));
|
||||||
|
assert!(!ForeignNetworkManager::should_reject_credential_trust_path(
|
||||||
|
PeerIdentityType::SharedNode
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn zero_digest_peer_cannot_bootstrap_foreign_network() {
|
||||||
|
let pm_center = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||||
|
set_secure_mode_cfg(&pm_center.get_global_ctx(), true);
|
||||||
|
|
||||||
|
let pma_net1 = create_mock_credential_peer_manager_for_foreign_network("net1").await;
|
||||||
|
|
||||||
|
let (a_ring, b_ring) = crate::tunnel::ring::create_ring_tunnel_pair();
|
||||||
|
let a_mgr_copy = pma_net1.clone();
|
||||||
|
let client = tokio::spawn(async move { a_mgr_copy.add_client_tunnel(a_ring, false).await });
|
||||||
|
let b_mgr_copy = pm_center.clone();
|
||||||
|
let server =
|
||||||
|
tokio::spawn(async move { b_mgr_copy.add_tunnel_as_server(b_ring, true).await });
|
||||||
|
|
||||||
|
assert!(client.await.unwrap().is_ok());
|
||||||
|
assert!(server.await.unwrap().is_err());
|
||||||
|
assert!(pm_center
|
||||||
|
.get_foreign_network_manager()
|
||||||
|
.list_foreign_networks()
|
||||||
|
.await
|
||||||
|
.foreign_networks
|
||||||
|
.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
async fn foreign_network_whitelist_helper(name: String) {
|
async fn foreign_network_whitelist_helper(name: String) {
|
||||||
let pm_center = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
let pm_center = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||||
tracing::debug!("pm_center: {:?}", pm_center.my_peer_id());
|
tracing::debug!("pm_center: {:?}", pm_center.my_peer_id());
|
||||||
|
|||||||
+109
-1
@@ -2,6 +2,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
use dashmap::{DashMap, DashSet};
|
use dashmap::{DashMap, DashSet};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
use tokio::{select, sync::mpsc};
|
use tokio::{select, sync::mpsc};
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ pub struct Peer {
|
|||||||
|
|
||||||
default_conn_id: Arc<AtomicCell<PeerConnId>>,
|
default_conn_id: Arc<AtomicCell<PeerConnId>>,
|
||||||
peer_identity_type: Arc<AtomicCell<Option<PeerIdentityType>>>,
|
peer_identity_type: Arc<AtomicCell<Option<PeerIdentityType>>>,
|
||||||
|
peer_public_key: Arc<RwLock<Option<Vec<u8>>>>,
|
||||||
default_conn_id_clear_task: ScopedTask<()>,
|
default_conn_id_clear_task: ScopedTask<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +58,8 @@ impl Peer {
|
|||||||
let shutdown_notifier = Arc::new(tokio::sync::Notify::new());
|
let shutdown_notifier = Arc::new(tokio::sync::Notify::new());
|
||||||
let peer_identity_type = Arc::new(AtomicCell::new(None));
|
let peer_identity_type = Arc::new(AtomicCell::new(None));
|
||||||
let peer_identity_type_copy = peer_identity_type.clone();
|
let peer_identity_type_copy = peer_identity_type.clone();
|
||||||
|
let peer_public_key = Arc::new(RwLock::new(None));
|
||||||
|
let peer_public_key_copy = peer_public_key.clone();
|
||||||
|
|
||||||
let conns_copy = conns.clone();
|
let conns_copy = conns.clone();
|
||||||
let shutdown_notifier_copy = shutdown_notifier.clone();
|
let shutdown_notifier_copy = shutdown_notifier.clone();
|
||||||
@@ -82,6 +86,7 @@ impl Peer {
|
|||||||
shrink_dashmap(&conns_copy, Some(4));
|
shrink_dashmap(&conns_copy, Some(4));
|
||||||
if conns_copy.is_empty() {
|
if conns_copy.is_empty() {
|
||||||
peer_identity_type_copy.store(None);
|
peer_identity_type_copy.store(None);
|
||||||
|
*peer_public_key_copy.write() = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,6 +131,7 @@ impl Peer {
|
|||||||
shutdown_notifier,
|
shutdown_notifier,
|
||||||
default_conn_id,
|
default_conn_id,
|
||||||
peer_identity_type,
|
peer_identity_type,
|
||||||
|
peer_public_key,
|
||||||
default_conn_id_clear_task,
|
default_conn_id_clear_task,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,6 +152,22 @@ impl Peer {
|
|||||||
|
|
||||||
let close_notifier = conn.get_close_notifier();
|
let close_notifier = conn.get_close_notifier();
|
||||||
let conn_info = conn.get_conn_info();
|
let conn_info = conn.get_conn_info();
|
||||||
|
let conn_pubkey = conn_info.noise_remote_static_pubkey.clone();
|
||||||
|
{
|
||||||
|
let mut peer_pubkey = self.peer_public_key.write();
|
||||||
|
if let Some(existing_pubkey) = peer_pubkey.as_ref() {
|
||||||
|
if existing_pubkey != &conn_pubkey {
|
||||||
|
return Err(Error::SecretKeyError(format!(
|
||||||
|
"peer public key mismatch. peer_id: {}, existing_len: {}, new_len: {}",
|
||||||
|
self.peer_node_id,
|
||||||
|
existing_pubkey.len(),
|
||||||
|
conn_pubkey.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*peer_pubkey = Some(conn_pubkey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
conn.start_recv_loop(self.packet_recv_chan.clone()).await;
|
conn.start_recv_loop(self.packet_recv_chan.clone()).await;
|
||||||
conn.start_pingpong();
|
conn.start_pingpong();
|
||||||
@@ -226,10 +248,14 @@ impl Peer {
|
|||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_live_conns(&self) -> bool {
|
||||||
|
self.conns.iter().any(|entry| !entry.value().is_closed())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has_directly_connected_conn(&self) -> bool {
|
pub fn has_directly_connected_conn(&self) -> bool {
|
||||||
self.conns
|
self.conns
|
||||||
.iter()
|
.iter()
|
||||||
.any(|entry| !(entry.value()).is_hole_punched())
|
.any(|entry| !entry.value().is_closed() && !entry.value().is_hole_punched())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_directly_connections(&self) -> DashSet<uuid::Uuid> {
|
pub fn get_directly_connections(&self) -> DashSet<uuid::Uuid> {
|
||||||
@@ -247,6 +273,10 @@ impl Peer {
|
|||||||
pub fn get_peer_identity_type(&self) -> Option<PeerIdentityType> {
|
pub fn get_peer_identity_type(&self) -> Option<PeerIdentityType> {
|
||||||
self.peer_identity_type.load()
|
self.peer_identity_type.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_peer_public_key(&self) -> Option<Vec<u8>> {
|
||||||
|
self.peer_public_key.read().clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pritn on drop
|
// pritn on drop
|
||||||
@@ -458,4 +488,82 @@ mod tests {
|
|||||||
let ret = peer.add_peer_conn(admin_client_conn).await;
|
let ret = peer.add_peer_conn(admin_client_conn).await;
|
||||||
assert!(ret.is_err());
|
assert!(ret.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn reject_peer_conn_with_mismatched_public_key() {
|
||||||
|
let (packet_send, _packet_recv) = create_packet_recv_chan();
|
||||||
|
let local_peer_id = new_peer_id();
|
||||||
|
let remote_peer_id = new_peer_id();
|
||||||
|
let peer = Peer::new(remote_peer_id, packet_send, get_mock_global_ctx());
|
||||||
|
let ps = Arc::new(PeerSessionStore::new());
|
||||||
|
|
||||||
|
let (client_tunnel_1, server_tunnel_1) = create_ring_tunnel_pair();
|
||||||
|
let client_ctx_1 = get_mock_global_ctx();
|
||||||
|
let server_ctx_1 = get_mock_global_ctx();
|
||||||
|
client_ctx_1
|
||||||
|
.config
|
||||||
|
.set_network_identity(NetworkIdentity::new("net1".to_string(), "sec1".to_string()));
|
||||||
|
server_ctx_1
|
||||||
|
.config
|
||||||
|
.set_network_identity(NetworkIdentity::new("net1".to_string(), "sec1".to_string()));
|
||||||
|
set_secure_mode_cfg(&client_ctx_1, true);
|
||||||
|
set_secure_mode_cfg(&server_ctx_1, true);
|
||||||
|
let mut client_conn_1 = PeerConn::new(
|
||||||
|
local_peer_id,
|
||||||
|
client_ctx_1,
|
||||||
|
Box::new(client_tunnel_1),
|
||||||
|
ps.clone(),
|
||||||
|
);
|
||||||
|
let mut server_conn_1 = PeerConn::new(
|
||||||
|
remote_peer_id,
|
||||||
|
server_ctx_1,
|
||||||
|
Box::new(server_tunnel_1),
|
||||||
|
ps.clone(),
|
||||||
|
);
|
||||||
|
let (c1, s1) = tokio::join!(
|
||||||
|
client_conn_1.do_handshake_as_client(),
|
||||||
|
server_conn_1.do_handshake_as_server()
|
||||||
|
);
|
||||||
|
c1.unwrap();
|
||||||
|
s1.unwrap();
|
||||||
|
|
||||||
|
let (client_tunnel_2, server_tunnel_2) = create_ring_tunnel_pair();
|
||||||
|
let client_ctx_2 = get_mock_global_ctx();
|
||||||
|
let server_ctx_2 = get_mock_global_ctx();
|
||||||
|
client_ctx_2
|
||||||
|
.config
|
||||||
|
.set_network_identity(NetworkIdentity::new("net1".to_string(), "sec1".to_string()));
|
||||||
|
server_ctx_2
|
||||||
|
.config
|
||||||
|
.set_network_identity(NetworkIdentity::new("net1".to_string(), "sec1".to_string()));
|
||||||
|
set_secure_mode_cfg(&client_ctx_2, true);
|
||||||
|
set_secure_mode_cfg(&server_ctx_2, true);
|
||||||
|
let mut client_conn_2 = PeerConn::new(
|
||||||
|
local_peer_id,
|
||||||
|
client_ctx_2,
|
||||||
|
Box::new(client_tunnel_2),
|
||||||
|
Arc::new(PeerSessionStore::new()),
|
||||||
|
);
|
||||||
|
let mut server_conn_2 = PeerConn::new(
|
||||||
|
remote_peer_id,
|
||||||
|
server_ctx_2,
|
||||||
|
Box::new(server_tunnel_2),
|
||||||
|
Arc::new(PeerSessionStore::new()),
|
||||||
|
);
|
||||||
|
let (c2, s2) = tokio::join!(
|
||||||
|
client_conn_2.do_handshake_as_client(),
|
||||||
|
server_conn_2.do_handshake_as_server()
|
||||||
|
);
|
||||||
|
c2.unwrap();
|
||||||
|
s2.unwrap();
|
||||||
|
|
||||||
|
let pubkey_1 = client_conn_1.get_conn_info().noise_remote_static_pubkey;
|
||||||
|
let pubkey_2 = client_conn_2.get_conn_info().noise_remote_static_pubkey;
|
||||||
|
assert_ne!(pubkey_1, pubkey_2);
|
||||||
|
|
||||||
|
peer.add_peer_conn(client_conn_1).await.unwrap();
|
||||||
|
assert_eq!(peer.get_peer_public_key(), Some(pubkey_1));
|
||||||
|
let ret = peer.add_peer_conn(client_conn_2).await;
|
||||||
|
assert!(ret.is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -455,6 +455,10 @@ impl PeerConn {
|
|||||||
self.is_hole_punched
|
self.is_hole_punched
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_closed(&self) -> bool {
|
||||||
|
self.close_event_notifier.is_closed()
|
||||||
|
}
|
||||||
|
|
||||||
async fn wait_handshake(&self, need_retry: &mut bool) -> Result<HandshakeRequest, Error> {
|
async fn wait_handshake(&self, need_retry: &mut bool) -> Result<HandshakeRequest, Error> {
|
||||||
*need_retry = false;
|
*need_retry = false;
|
||||||
|
|
||||||
@@ -687,9 +691,9 @@ impl PeerConn {
|
|||||||
/// | Admin | Admin | same network_secret, proof verified | NetworkSecretConfirmed | NetworkSecretConfirmed | Admin | Admin |
|
/// | Admin | Admin | same network_secret, proof verified | NetworkSecretConfirmed | NetworkSecretConfirmed | Admin | Admin |
|
||||||
/// | Credential | Admin | client pubkey is trusted by admin | EncryptedUnauthenticated | PeerVerified | Admin | Credential |
|
/// | Credential | Admin | client pubkey is trusted by admin | EncryptedUnauthenticated | PeerVerified | Admin | Credential |
|
||||||
/// | Credential | Admin | client pubkey is unknown | handshake may fail | handshake reject | unknown | unknown |
|
/// | Credential | Admin | client pubkey is unknown | handshake may fail | handshake reject | unknown | unknown |
|
||||||
/// | Admin | SharedNode | pinned key match | PeerVerified | EncryptedUnauthenticated | SharedNode | SharedNode |
|
/// | Admin | SharedNode | pinned key match | PeerVerified | EncryptedUnauthenticated | SharedNode | Admin |
|
||||||
/// | Admin | SharedNode | local has no pinned key requirement | EncryptedUnauthenticated | EncryptedUnauthenticated | SharedNode | SharedNode |
|
/// | Admin | SharedNode | local has no pinned key requirement | EncryptedUnauthenticated | EncryptedUnauthenticated | SharedNode | Admin |
|
||||||
/// | Credential | SharedNode | no pin and not trusted | EncryptedUnauthenticated | EncryptedUnauthenticated | SharedNode | SharedNode |
|
/// | Credential | SharedNode | no pin and not trusted | EncryptedUnauthenticated | EncryptedUnauthenticated | SharedNode | Credential |
|
||||||
/// | Credential | Credential | should reject | handshake reject | handshake reject | unknown | unknown |
|
/// | Credential | Credential | should reject | handshake reject | handshake reject | unknown | unknown |
|
||||||
///
|
///
|
||||||
/// Logic (in priority order):
|
/// Logic (in priority order):
|
||||||
@@ -764,13 +768,19 @@ impl PeerConn {
|
|||||||
secure_auth_level: SecureAuthLevel,
|
secure_auth_level: SecureAuthLevel,
|
||||||
remote_role_hint_is_same_network: bool,
|
remote_role_hint_is_same_network: bool,
|
||||||
remote_sent_secret_proof: bool,
|
remote_sent_secret_proof: bool,
|
||||||
|
is_client: bool,
|
||||||
) -> PeerIdentityType {
|
) -> PeerIdentityType {
|
||||||
if !remote_role_hint_is_same_network
|
if !remote_role_hint_is_same_network
|
||||||
|| remote_network_name != self.global_ctx.get_network_name()
|
|| remote_network_name != self.global_ctx.get_network_name()
|
||||||
{
|
{
|
||||||
return PeerIdentityType::SharedNode;
|
if is_client {
|
||||||
|
PeerIdentityType::SharedNode
|
||||||
|
} else if remote_sent_secret_proof {
|
||||||
|
PeerIdentityType::Admin
|
||||||
|
} else {
|
||||||
|
PeerIdentityType::Credential
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
if matches!(secure_auth_level, SecureAuthLevel::NetworkSecretConfirmed)
|
if matches!(secure_auth_level, SecureAuthLevel::NetworkSecretConfirmed)
|
||||||
|| remote_sent_secret_proof
|
|| remote_sent_secret_proof
|
||||||
{
|
{
|
||||||
@@ -779,6 +789,7 @@ impl PeerConn {
|
|||||||
|
|
||||||
PeerIdentityType::Credential
|
PeerIdentityType::Credential
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn do_noise_handshake_as_client(&self) -> Result<NoiseHandshakeResult, Error> {
|
async fn do_noise_handshake_as_client(&self) -> Result<NoiseHandshakeResult, Error> {
|
||||||
let prologue = b"easytier-peerconn-noise".to_vec();
|
let prologue = b"easytier-peerconn-noise".to_vec();
|
||||||
@@ -920,6 +931,7 @@ impl PeerConn {
|
|||||||
secure_auth_level,
|
secure_auth_level,
|
||||||
msg2_pb.role_hint == 1,
|
msg2_pb.role_hint == 1,
|
||||||
remote_sent_secret_proof,
|
remote_sent_secret_proof,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
let handshake_hash = hs.get_handshake_hash().to_vec();
|
let handshake_hash = hs.get_handshake_hash().to_vec();
|
||||||
@@ -1174,6 +1186,7 @@ impl PeerConn {
|
|||||||
secure_auth_level,
|
secure_auth_level,
|
||||||
role_hint == 1,
|
role_hint == 1,
|
||||||
msg3_pb.secret_proof_32.is_some(),
|
msg3_pb.secret_proof_32.is_some(),
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let handshake_hash = hs.get_handshake_hash().to_vec();
|
let handshake_hash = hs.get_handshake_hash().to_vec();
|
||||||
@@ -1948,7 +1961,7 @@ pub mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
s_peer.get_conn_info().peer_identity_type,
|
s_peer.get_conn_info().peer_identity_type,
|
||||||
PeerIdentityType::SharedNode as i32,
|
PeerIdentityType::Admin as i32,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1999,7 +2012,7 @@ pub mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
s_peer.get_conn_info().peer_identity_type,
|
s_peer.get_conn_info().peer_identity_type,
|
||||||
PeerIdentityType::SharedNode as i32,
|
PeerIdentityType::Admin as i32,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -819,17 +819,15 @@ impl PeerManager {
|
|||||||
tracing::error!(?e, "decrypt failed");
|
tracing::error!(?e, "decrypt failed");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else if !peers.has_peer(from_peer_id)
|
} else if hdr.is_encrypted() {
|
||||||
&& !foreign_client.has_next_hop(from_peer_id)
|
|
||||||
{
|
|
||||||
match relay_peer_map.decrypt_if_needed(&mut ret).await {
|
match relay_peer_map.decrypt_if_needed(&mut ret).await {
|
||||||
Ok(true) => {}
|
Ok(true) => {}
|
||||||
Ok(false) => {
|
Ok(false) => {
|
||||||
tracing::error!("relay session not found");
|
tracing::error!("secure session not found");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(?e, "relay decrypt failed");
|
tracing::error!(?e, "secure decrypt failed");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -904,6 +902,16 @@ impl PeerManager {
|
|||||||
async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option<ZCPacket> {
|
async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option<ZCPacket> {
|
||||||
let hdr = packet.peer_manager_header().unwrap();
|
let hdr = packet.peer_manager_header().unwrap();
|
||||||
if hdr.packet_type == PacketType::Data as u8 && !hdr.is_not_send_to_tun() {
|
if hdr.packet_type == PacketType::Data as u8 && !hdr.is_not_send_to_tun() {
|
||||||
|
if hdr.is_encrypted() || hdr.is_compressed() {
|
||||||
|
tracing::warn!(
|
||||||
|
from_peer_id = hdr.from_peer_id.get(),
|
||||||
|
to_peer_id = hdr.to_peer_id.get(),
|
||||||
|
encrypted = hdr.is_encrypted(),
|
||||||
|
compressed = hdr.is_compressed(),
|
||||||
|
"dropping packet before nic because it is not fully decoded"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
tracing::trace!(?packet, "send packet to nic channel");
|
tracing::trace!(?packet, "send packet to nic channel");
|
||||||
// TODO: use a function to get the body ref directly for zero copy
|
// TODO: use a function to get the body ref directly for zero copy
|
||||||
let _ = self.nic_channel.send(packet).await;
|
let _ = self.nic_channel.send(packet).await;
|
||||||
@@ -989,6 +997,11 @@ impl PeerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_peer_public_key(&self, peer_id: PeerId) -> Option<Vec<u8>> {
|
||||||
|
let peer_map = self.peers.upgrade()?;
|
||||||
|
peer_map.get_peer_public_key(peer_id)
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_peer_identity_type(&self, peer_id: PeerId) -> Option<PeerIdentityType> {
|
async fn get_peer_identity_type(&self, peer_id: PeerId) -> Option<PeerIdentityType> {
|
||||||
let peer_map = self.peers.upgrade()?;
|
let peer_map = self.peers.upgrade()?;
|
||||||
peer_map.get_peer_identity_type(peer_id)
|
peer_map.get_peer_identity_type(peer_id)
|
||||||
|
|||||||
@@ -278,13 +278,9 @@ impl PeerMap {
|
|||||||
|
|
||||||
pub async fn list_peers_with_conn(&self) -> Vec<PeerId> {
|
pub async fn list_peers_with_conn(&self) -> Vec<PeerId> {
|
||||||
let mut ret = Vec::new();
|
let mut ret = Vec::new();
|
||||||
let peers = self.list_peers();
|
for item in self.peer_map.iter() {
|
||||||
for peer_id in peers.iter() {
|
if item.value().has_live_conns() {
|
||||||
let Some(peer) = self.get_peer_by_id(*peer_id) else {
|
ret.push(*item.key());
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if !peer.list_peer_conns().await.is_empty() {
|
|
||||||
ret.push(*peer_id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ret
|
ret
|
||||||
@@ -308,6 +304,11 @@ impl PeerMap {
|
|||||||
.and_then(|p| p.get_peer_identity_type())
|
.and_then(|p| p.get_peer_identity_type())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_peer_public_key(&self, peer_id: PeerId) -> Option<Vec<u8>> {
|
||||||
|
self.get_peer_by_id(peer_id)
|
||||||
|
.and_then(|p| p.get_peer_public_key())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn close_peer_conn(
|
pub async fn close_peer_conn(
|
||||||
&self,
|
&self,
|
||||||
peer_id: PeerId,
|
peer_id: PeerId,
|
||||||
|
|||||||
@@ -32,8 +32,12 @@ use tokio::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{
|
common::{
|
||||||
config::NetworkIdentity, constants::EASYTIER_VERSION, global_ctx::ArcGlobalCtx,
|
config::NetworkIdentity,
|
||||||
shrink_dashmap, stun::StunInfoCollectorTrait, PeerId,
|
constants::EASYTIER_VERSION,
|
||||||
|
global_ctx::{ArcGlobalCtx, GlobalCtxEvent},
|
||||||
|
shrink_dashmap,
|
||||||
|
stun::StunInfoCollectorTrait,
|
||||||
|
PeerId,
|
||||||
},
|
},
|
||||||
peers::route_trait::{Route, RouteInterfaceBox},
|
peers::route_trait::{Route, RouteInterfaceBox},
|
||||||
proto::{
|
proto::{
|
||||||
@@ -66,6 +70,8 @@ use super::{
|
|||||||
PeerPacketFilter,
|
PeerPacketFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use atomic_shim::AtomicU64;
|
||||||
|
|
||||||
static SERVICE_ID: u32 = 7;
|
static SERVICE_ID: u32 = 7;
|
||||||
static UPDATE_PEER_INFO_PERIOD: Duration = Duration::from_secs(3600);
|
static UPDATE_PEER_INFO_PERIOD: Duration = Duration::from_secs(3600);
|
||||||
static REMOVE_DEAD_PEER_INFO_AFTER: Duration = Duration::from_secs(3660);
|
static REMOVE_DEAD_PEER_INFO_AFTER: Duration = Duration::from_secs(3660);
|
||||||
@@ -371,6 +377,13 @@ impl Default for RouteConnInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct InterfacePeerSnapshot {
|
||||||
|
generation: u64,
|
||||||
|
peers: BTreeSet<PeerId>,
|
||||||
|
identity_types: BTreeMap<PeerId, Option<PeerIdentityType>>,
|
||||||
|
}
|
||||||
|
|
||||||
// constructed with all infos synced from all peers.
|
// constructed with all infos synced from all peers.
|
||||||
struct SyncedRouteInfo {
|
struct SyncedRouteInfo {
|
||||||
peer_infos: RwLock<OrderedHashMap<PeerId, RoutePeerInfo>>,
|
peer_infos: RwLock<OrderedHashMap<PeerId, RoutePeerInfo>>,
|
||||||
@@ -1049,14 +1062,12 @@ impl SyncedRouteInfo {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_credential_info(&self, peer_id: PeerId) -> Option<TrustedCredentialPubkey> {
|
fn get_credential_info_by_pubkey(&self, peer_pubkey: &[u8]) -> Option<TrustedCredentialPubkey> {
|
||||||
let peer_infos = self.peer_infos.read();
|
if peer_pubkey.is_empty() {
|
||||||
let info = peer_infos.get(&peer_id)?;
|
|
||||||
if info.noise_static_pubkey.is_empty() {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
self.trusted_credential_pubkeys
|
self.trusted_credential_pubkeys
|
||||||
.get(&info.noise_static_pubkey)
|
.get(peer_pubkey)
|
||||||
.map(|r| r.value().clone())
|
.map(|r| r.value().clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1800,6 +1811,9 @@ struct PeerRouteServiceImpl {
|
|||||||
synced_route_info: SyncedRouteInfo,
|
synced_route_info: SyncedRouteInfo,
|
||||||
cached_local_conn_map: std::sync::Mutex<RouteConnBitmap>,
|
cached_local_conn_map: std::sync::Mutex<RouteConnBitmap>,
|
||||||
cached_local_conn_map_version: AtomicVersion,
|
cached_local_conn_map_version: AtomicVersion,
|
||||||
|
cached_interface_peer_snapshot: std::sync::Mutex<Arc<InterfacePeerSnapshot>>,
|
||||||
|
interface_peers_generation: AtomicU64,
|
||||||
|
applied_interface_peers_generation: AtomicU64,
|
||||||
|
|
||||||
last_update_my_foreign_network: AtomicCell<Option<std::time::Instant>>,
|
last_update_my_foreign_network: AtomicCell<Option<std::time::Instant>>,
|
||||||
|
|
||||||
@@ -1858,6 +1872,11 @@ impl PeerRouteServiceImpl {
|
|||||||
},
|
},
|
||||||
cached_local_conn_map: std::sync::Mutex::new(RouteConnBitmap::default()),
|
cached_local_conn_map: std::sync::Mutex::new(RouteConnBitmap::default()),
|
||||||
cached_local_conn_map_version: AtomicVersion::new(),
|
cached_local_conn_map_version: AtomicVersion::new(),
|
||||||
|
cached_interface_peer_snapshot: std::sync::Mutex::new(Arc::new(
|
||||||
|
InterfacePeerSnapshot::default(),
|
||||||
|
)),
|
||||||
|
interface_peers_generation: AtomicU64::new(1),
|
||||||
|
applied_interface_peers_generation: AtomicU64::new(0),
|
||||||
|
|
||||||
last_update_my_foreign_network: AtomicCell::new(None),
|
last_update_my_foreign_network: AtomicCell::new(None),
|
||||||
|
|
||||||
@@ -1904,15 +1923,60 @@ impl PeerRouteServiceImpl {
|
|||||||
self.sessions.iter().map(|x| *x.key()).collect()
|
self.sessions.iter().map(|x| *x.key()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mark_interface_peers_dirty(&self) {
|
||||||
|
self.interface_peers_generation
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn interface_peer_snapshot_uncached(&self) -> InterfacePeerSnapshot {
|
||||||
|
let interface = self.interface.lock().await;
|
||||||
|
let interface = interface.as_ref().unwrap();
|
||||||
|
|
||||||
|
let peers: BTreeSet<_> = interface.list_peers().await.into_iter().collect();
|
||||||
|
let mut identity_types = BTreeMap::new();
|
||||||
|
for peer_id in peers.iter().copied() {
|
||||||
|
identity_types.insert(peer_id, interface.get_peer_identity_type(peer_id).await);
|
||||||
|
}
|
||||||
|
|
||||||
|
InterfacePeerSnapshot {
|
||||||
|
generation: 0,
|
||||||
|
peers,
|
||||||
|
identity_types,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn interface_peer_snapshot(&self) -> Arc<InterfacePeerSnapshot> {
|
||||||
|
loop {
|
||||||
|
let start_generation = self.interface_peers_generation.load(Ordering::Acquire);
|
||||||
|
{
|
||||||
|
let cached = self.cached_interface_peer_snapshot.lock().unwrap();
|
||||||
|
if cached.generation == start_generation {
|
||||||
|
return cached.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut snapshot = self.interface_peer_snapshot_uncached().await;
|
||||||
|
let end_generation = self.interface_peers_generation.load(Ordering::Acquire);
|
||||||
|
if start_generation == end_generation {
|
||||||
|
snapshot.generation = end_generation;
|
||||||
|
let snapshot = Arc::new(snapshot);
|
||||||
|
*self.cached_interface_peer_snapshot.lock().unwrap() = snapshot.clone();
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_peers_from_interface_snapshot(&self) -> (u64, BTreeSet<PeerId>) {
|
||||||
|
let snapshot = self.interface_peer_snapshot().await;
|
||||||
|
(snapshot.generation, snapshot.peers.clone())
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_peers_from_interface<T: FromIterator<PeerId>>(&self) -> T {
|
async fn list_peers_from_interface<T: FromIterator<PeerId>>(&self) -> T {
|
||||||
self.interface
|
self.interface_peer_snapshot()
|
||||||
.lock()
|
|
||||||
.await
|
.await
|
||||||
.as_ref()
|
.peers
|
||||||
.unwrap()
|
.iter()
|
||||||
.list_peers()
|
.copied()
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1920,6 +1984,11 @@ impl PeerRouteServiceImpl {
|
|||||||
&self,
|
&self,
|
||||||
peer_id: PeerId,
|
peer_id: PeerId,
|
||||||
) -> Option<PeerIdentityType> {
|
) -> Option<PeerIdentityType> {
|
||||||
|
let snapshot = self.interface_peer_snapshot().await;
|
||||||
|
if let Some(identity_type) = snapshot.identity_types.get(&peer_id) {
|
||||||
|
return *identity_type;
|
||||||
|
}
|
||||||
|
|
||||||
self.interface
|
self.interface
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
@@ -1929,6 +1998,16 @@ impl PeerRouteServiceImpl {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_peer_public_key_from_interface(&self, peer_id: PeerId) -> Option<Vec<u8>> {
|
||||||
|
self.interface
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.get_peer_public_key(peer_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
fn update_my_peer_info(&self) -> bool {
|
fn update_my_peer_info(&self) -> bool {
|
||||||
self.synced_route_info.update_my_peer_info(
|
self.synced_route_info.update_my_peer_info(
|
||||||
self.my_peer_id,
|
self.my_peer_id,
|
||||||
@@ -1938,9 +2017,33 @@ impl PeerRouteServiceImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn update_my_conn_info(&self) -> bool {
|
async fn update_my_conn_info(&self) -> bool {
|
||||||
let connected_peers: BTreeSet<PeerId> = self.list_peers_from_interface().await;
|
let current_generation = self.interface_peers_generation.load(Ordering::Acquire);
|
||||||
self.synced_route_info
|
let generation_applied = self
|
||||||
.update_my_conn_info(self.my_peer_id, connected_peers)
|
.applied_interface_peers_generation
|
||||||
|
.load(Ordering::Acquire)
|
||||||
|
== current_generation;
|
||||||
|
if generation_applied {
|
||||||
|
let need_periodic_requery = self
|
||||||
|
.interface
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.as_ref()
|
||||||
|
.map(|x| x.need_periodic_requery_peers())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !need_periodic_requery {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mark_interface_peers_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (generation, connected_peers) = self.list_peers_from_interface_snapshot().await;
|
||||||
|
let updated = self
|
||||||
|
.synced_route_info
|
||||||
|
.update_my_conn_info(self.my_peer_id, connected_peers);
|
||||||
|
self.applied_interface_peers_generation
|
||||||
|
.store(generation, Ordering::Release);
|
||||||
|
updated
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_my_foreign_network(&self) -> bool {
|
async fn update_my_foreign_network(&self) -> bool {
|
||||||
@@ -2048,6 +2151,18 @@ impl PeerRouteServiceImpl {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_global_ctx_event(&self, event: &GlobalCtxEvent) {
|
||||||
|
if matches!(
|
||||||
|
event,
|
||||||
|
GlobalCtxEvent::PeerAdded(_)
|
||||||
|
| GlobalCtxEvent::PeerRemoved(_)
|
||||||
|
| GlobalCtxEvent::PeerConnAdded(_)
|
||||||
|
| GlobalCtxEvent::PeerConnRemoved(_)
|
||||||
|
) {
|
||||||
|
self.mark_interface_peers_dirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn update_route_table_and_cached_local_conn_bitmap(&self) {
|
fn update_route_table_and_cached_local_conn_bitmap(&self) {
|
||||||
self.update_peer_info_last_update();
|
self.update_peer_info_last_update();
|
||||||
|
|
||||||
@@ -2283,15 +2398,7 @@ impl PeerRouteServiceImpl {
|
|||||||
let my_foreign_network_updated = self.update_my_foreign_network().await;
|
let my_foreign_network_updated = self.update_my_foreign_network().await;
|
||||||
let mut untrusted_changed = false;
|
let mut untrusted_changed = false;
|
||||||
if my_peer_info_updated {
|
if my_peer_info_updated {
|
||||||
let network_identity = self.global_ctx.get_network_identity();
|
untrusted_changed = self.refresh_credential_trusts_and_disconnect().await;
|
||||||
let network_secret = network_identity.network_secret.as_deref();
|
|
||||||
let (untrusted, global_trusted_keys) = self
|
|
||||||
.synced_route_info
|
|
||||||
.verify_and_update_credential_trusts(network_secret);
|
|
||||||
self.global_ctx
|
|
||||||
.update_trusted_keys(global_trusted_keys, &network_identity.network_name);
|
|
||||||
self.disconnect_untrusted_peers(&untrusted).await;
|
|
||||||
untrusted_changed = !untrusted.is_empty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if my_peer_info_updated || my_conn_info_updated || untrusted_changed {
|
if my_peer_info_updated || my_conn_info_updated || untrusted_changed {
|
||||||
@@ -2304,6 +2411,18 @@ impl PeerRouteServiceImpl {
|
|||||||
my_peer_info_updated || my_conn_info_updated || my_foreign_network_updated
|
my_peer_info_updated || my_conn_info_updated || my_foreign_network_updated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn refresh_credential_trusts_and_disconnect(&self) -> bool {
|
||||||
|
let network_identity = self.global_ctx.get_network_identity();
|
||||||
|
let network_secret = network_identity.network_secret.as_deref();
|
||||||
|
let (untrusted, global_trusted_keys) = self
|
||||||
|
.synced_route_info
|
||||||
|
.verify_and_update_credential_trusts(network_secret);
|
||||||
|
self.global_ctx
|
||||||
|
.update_trusted_keys(global_trusted_keys, &network_identity.network_name);
|
||||||
|
self.disconnect_untrusted_peers(&untrusted).await;
|
||||||
|
!untrusted.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
async fn disconnect_untrusted_peers(&self, untrusted_peers: &[PeerId]) {
|
async fn disconnect_untrusted_peers(&self, untrusted_peers: &[PeerId]) {
|
||||||
if untrusted_peers.is_empty() {
|
if untrusted_peers.is_empty() {
|
||||||
return;
|
return;
|
||||||
@@ -2364,7 +2483,7 @@ impl PeerRouteServiceImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_expired_peer(&self) {
|
async fn clear_expired_peer(&self) {
|
||||||
let now = SystemTime::now();
|
let now = SystemTime::now();
|
||||||
let mut to_remove = Vec::new();
|
let mut to_remove = Vec::new();
|
||||||
for (peer_id, peer_info) in self.synced_route_info.peer_infos.read().iter() {
|
for (peer_id, peer_info) in self.synced_route_info.peer_infos.read().iter() {
|
||||||
@@ -2404,6 +2523,7 @@ impl PeerRouteServiceImpl {
|
|||||||
self.synced_route_info.foreign_network.remove(p);
|
self.synced_route_info.foreign_network.remove(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.refresh_credential_trusts_and_disconnect().await;
|
||||||
self.route_table.clean_expired_route_info();
|
self.route_table.clean_expired_route_info();
|
||||||
self.route_table_with_cost.clean_expired_route_info();
|
self.route_table_with_cost.clean_expired_route_info();
|
||||||
}
|
}
|
||||||
@@ -2789,10 +2909,9 @@ impl RouteSessionManager {
|
|||||||
_ = recv.recv() => {}
|
_ = recv.recv() => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut peers = service_impl.list_peers_from_interface::<Vec<_>>().await;
|
let interface_snapshot = service_impl.interface_peer_snapshot().await;
|
||||||
peers.sort();
|
let peers = &interface_snapshot.peers;
|
||||||
|
let session_peers = self.list_session_peer_set();
|
||||||
let session_peers = self.list_session_peers();
|
|
||||||
for peer_id in session_peers.iter() {
|
for peer_id in session_peers.iter() {
|
||||||
if !peers.contains(peer_id) {
|
if !peers.contains(peer_id) {
|
||||||
if Some(*peer_id) == cur_dst_peer_id_to_initiate {
|
if Some(*peer_id) == cur_dst_peer_id_to_initiate {
|
||||||
@@ -2808,9 +2927,11 @@ impl RouteSessionManager {
|
|||||||
// Step 9a: Filter OSPF session candidates based on direct auth level.
|
// Step 9a: Filter OSPF session candidates based on direct auth level.
|
||||||
// - Credential nodes only initiate sessions to admin nodes (not other credential nodes)
|
// - Credential nodes only initiate sessions to admin nodes (not other credential nodes)
|
||||||
// - Admin nodes don't initiate sessions to credential nodes
|
// - Admin nodes don't initiate sessions to credential nodes
|
||||||
let identity_type = service_impl
|
let identity_type = interface_snapshot
|
||||||
.get_peer_identity_type_from_interface(peer_id)
|
.identity_types
|
||||||
.await
|
.get(&peer_id)
|
||||||
|
.copied()
|
||||||
|
.flatten()
|
||||||
.unwrap_or(PeerIdentityType::Admin);
|
.unwrap_or(PeerIdentityType::Admin);
|
||||||
if matches!(identity_type, PeerIdentityType::Credential) {
|
if matches!(identity_type, PeerIdentityType::Credential) {
|
||||||
continue;
|
continue;
|
||||||
@@ -2896,6 +3017,14 @@ impl RouteSessionManager {
|
|||||||
service_impl.list_session_peers()
|
service_impl.list_session_peers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn list_session_peer_set(&self) -> BTreeSet<PeerId> {
|
||||||
|
let Some(service_impl) = self.service_impl.upgrade() else {
|
||||||
|
return BTreeSet::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
service_impl.list_session_peers().into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn dump_sessions(&self) -> Result<String, Error> {
|
fn dump_sessions(&self) -> Result<String, Error> {
|
||||||
let Some(service_impl) = self.service_impl.upgrade() else {
|
let Some(service_impl) = self.service_impl.upgrade() else {
|
||||||
return Err(Error::Stopped);
|
return Err(Error::Stopped);
|
||||||
@@ -2919,6 +3048,33 @@ impl RouteSessionManager {
|
|||||||
tracing::debug!(?ret, ?reason, "sync_now_broadcast.send");
|
tracing::debug!(?ret, ?reason, "sync_now_broadcast.send");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extract_credential_peer_info(
|
||||||
|
&self,
|
||||||
|
from_peer_id: PeerId,
|
||||||
|
peer_infos: &[RoutePeerInfo],
|
||||||
|
raw_peer_infos: &[DynamicMessage],
|
||||||
|
credential: &TrustedCredentialPubkey,
|
||||||
|
) -> Option<(RoutePeerInfo, DynamicMessage)> {
|
||||||
|
let info_idx = peer_infos.iter().position(|p| p.peer_id == from_peer_id)?;
|
||||||
|
let mut info = peer_infos[info_idx].clone();
|
||||||
|
let mut raw_info = raw_peer_infos[info_idx].clone();
|
||||||
|
let allowed_cidrs = &credential.allowed_proxy_cidrs;
|
||||||
|
// Filter proxy_cidrs to only those allowed by credential
|
||||||
|
if !allowed_cidrs.is_empty() {
|
||||||
|
info.proxy_cidrs.retain(|cidr| {
|
||||||
|
allowed_cidrs
|
||||||
|
.iter()
|
||||||
|
.any(|allowed| cidr_is_subset_str(cidr, allowed))
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No allowed_proxy_cidrs → no proxy_cidrs allowed
|
||||||
|
info.proxy_cidrs.clear();
|
||||||
|
}
|
||||||
|
SyncedRouteInfo::mark_credential_peer(&mut info, true);
|
||||||
|
patch_raw_from_info(&mut raw_info, &info, &["proxy_cidrs", "feature_flag"]);
|
||||||
|
Some((info, raw_info))
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn do_sync_route_info(
|
async fn do_sync_route_info(
|
||||||
&self,
|
&self,
|
||||||
@@ -2942,7 +3098,22 @@ impl RouteSessionManager {
|
|||||||
.await
|
.await
|
||||||
.unwrap_or(PeerIdentityType::Admin);
|
.unwrap_or(PeerIdentityType::Admin);
|
||||||
let from_is_credential = matches!(from_identity_type, PeerIdentityType::Credential);
|
let from_is_credential = matches!(from_identity_type, PeerIdentityType::Credential);
|
||||||
let from_is_shared = matches!(from_identity_type, PeerIdentityType::SharedNode);
|
let credential_info = if from_is_credential {
|
||||||
|
service_impl
|
||||||
|
.get_peer_public_key_from_interface(from_peer_id)
|
||||||
|
.await
|
||||||
|
.and_then(|pubkey| {
|
||||||
|
service_impl
|
||||||
|
.synced_route_info
|
||||||
|
.get_credential_info_by_pubkey(&pubkey)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if from_is_credential && credential_info.is_none() {
|
||||||
|
// no credential found
|
||||||
|
return Err(Error::Stopped);
|
||||||
|
}
|
||||||
|
|
||||||
let _session_lock = session.lock.lock();
|
let _session_lock = session.lock.lock();
|
||||||
|
|
||||||
@@ -2955,80 +3126,22 @@ impl RouteSessionManager {
|
|||||||
|
|
||||||
if let Some(peer_infos) = &peer_infos {
|
if let Some(peer_infos) = &peer_infos {
|
||||||
// Step 9b: credential peers can only propagate their own route info
|
// Step 9b: credential peers can only propagate their own route info
|
||||||
let normalize_raw = |info: &RoutePeerInfo| {
|
// patch_raw_from_info(&mut raw, info, &["proxy_cidrs", "feature_flag"]);
|
||||||
let mut raw = DynamicMessage::new(RoutePeerInfo::default().descriptor());
|
|
||||||
raw.transcode_from(info).unwrap();
|
|
||||||
raw
|
|
||||||
};
|
|
||||||
let normalized_peer_infos: Vec<RoutePeerInfo>;
|
|
||||||
let normalized_raw_peer_infos: Vec<DynamicMessage>;
|
|
||||||
let (pi, rpi) = if from_is_credential {
|
let (pi, rpi) = if from_is_credential {
|
||||||
let allowed_cidrs = service_impl
|
if let Some(ret) = self.extract_credential_peer_info(
|
||||||
.synced_route_info
|
from_peer_id,
|
||||||
.get_credential_info(from_peer_id)
|
peer_infos,
|
||||||
.map(|tc| tc.allowed_proxy_cidrs.clone())
|
raw_peer_infos.as_deref().unwrap(),
|
||||||
.unwrap_or_default();
|
credential_info.as_ref().unwrap(),
|
||||||
normalized_peer_infos = peer_infos
|
) {
|
||||||
.iter()
|
(&vec![ret.0], &vec![ret.1])
|
||||||
.filter(|info| info.peer_id == from_peer_id)
|
|
||||||
.cloned()
|
|
||||||
.map(|mut info| {
|
|
||||||
// Filter proxy_cidrs to only those allowed by credential
|
|
||||||
if !allowed_cidrs.is_empty() {
|
|
||||||
info.proxy_cidrs.retain(|cidr| {
|
|
||||||
allowed_cidrs
|
|
||||||
.iter()
|
|
||||||
.any(|allowed| cidr_is_subset_str(cidr, allowed))
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// No allowed_proxy_cidrs → no proxy_cidrs allowed
|
(&vec![], &vec![])
|
||||||
info.proxy_cidrs.clear();
|
|
||||||
}
|
}
|
||||||
SyncedRouteInfo::mark_credential_peer(&mut info, true);
|
|
||||||
info
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
normalized_raw_peer_infos = normalized_peer_infos
|
|
||||||
.iter()
|
|
||||||
.map(|info| {
|
|
||||||
// Find original raw for this peer to preserve unknown fields
|
|
||||||
let orig_idx = peer_infos.iter().position(|p| p.peer_id == info.peer_id);
|
|
||||||
let mut raw = orig_idx
|
|
||||||
.and_then(|idx| raw_peer_infos.as_ref().map(|rpi| rpi[idx].clone()))
|
|
||||||
.unwrap_or_else(|| normalize_raw(info));
|
|
||||||
patch_raw_from_info(&mut raw, info, &["proxy_cidrs", "feature_flag"]);
|
|
||||||
raw
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
(&normalized_peer_infos, &normalized_raw_peer_infos)
|
|
||||||
} else {
|
} else {
|
||||||
let mut peer_infos_mut = peer_infos.clone();
|
(peer_infos, raw_peer_infos.as_ref().unwrap())
|
||||||
let mut raw_peer_infos_mut = raw_peer_infos
|
|
||||||
.as_ref()
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| peer_infos_mut.iter().map(normalize_raw).collect());
|
|
||||||
if from_is_shared {
|
|
||||||
for (info, raw) in peer_infos_mut.iter_mut().zip(raw_peer_infos_mut.iter_mut())
|
|
||||||
{
|
|
||||||
info.trusted_credential_pubkeys.clear();
|
|
||||||
patch_raw_from_info(raw, info, &["trusted_credential_pubkeys"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some((idx, info)) = peer_infos_mut
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.find(|(_, info)| info.peer_id == from_peer_id)
|
|
||||||
{
|
|
||||||
let mut info = info.clone();
|
|
||||||
SyncedRouteInfo::mark_credential_peer(&mut info, false);
|
|
||||||
peer_infos_mut[idx] = info.clone();
|
|
||||||
patch_raw_from_info(&mut raw_peer_infos_mut[idx], &info, &["feature_flag"]);
|
|
||||||
}
|
|
||||||
normalized_peer_infos = peer_infos_mut;
|
|
||||||
normalized_raw_peer_infos = raw_peer_infos_mut;
|
|
||||||
(&normalized_peer_infos, &normalized_raw_peer_infos)
|
|
||||||
};
|
};
|
||||||
|
if !pi.is_empty() {
|
||||||
service_impl.synced_route_info.update_peer_infos(
|
service_impl.synced_route_info.update_peer_infos(
|
||||||
my_peer_id,
|
my_peer_id,
|
||||||
service_impl.my_peer_route_id,
|
service_impl.my_peer_route_id,
|
||||||
@@ -3045,18 +3158,12 @@ impl RouteSessionManager {
|
|||||||
session.update_dst_saved_peer_info_version(pi, from_peer_id);
|
session.update_dst_saved_peer_info_version(pi, from_peer_id);
|
||||||
need_update_route_table = true;
|
need_update_route_table = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 9b: credential peers' conn_info depends on allow_relay flag
|
// Step 9b: credential peers' conn_info depends on allow_relay flag
|
||||||
if let Some(conn_info) = &conn_info {
|
if let Some(conn_info) = &conn_info {
|
||||||
let accept_conn_info = if from_is_credential {
|
let accept_conn_info =
|
||||||
service_impl
|
!from_is_credential || credential_info.map(|tc| tc.allow_relay).unwrap_or(false);
|
||||||
.synced_route_info
|
|
||||||
.get_credential_info(from_peer_id)
|
|
||||||
.map(|tc| tc.allow_relay)
|
|
||||||
.unwrap_or(false)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
};
|
|
||||||
if accept_conn_info {
|
if accept_conn_info {
|
||||||
service_impl.synced_route_info.update_conn_info(conn_info);
|
service_impl.synced_route_info.update_conn_info(conn_info);
|
||||||
session.update_dst_saved_conn_info_version(conn_info, from_peer_id);
|
session.update_dst_saved_conn_info_version(conn_info, from_peer_id);
|
||||||
@@ -3162,7 +3269,7 @@ impl PeerRoute {
|
|||||||
async fn clear_expired_peer(service_impl: Arc<PeerRouteServiceImpl>) {
|
async fn clear_expired_peer(service_impl: Arc<PeerRouteServiceImpl>) {
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||||
service_impl.clear_expired_peer();
|
service_impl.clear_expired_peer().await;
|
||||||
// TODO: use debug log level for this.
|
// TODO: use debug log level for this.
|
||||||
tracing::debug!(?service_impl, "clear_expired_peer");
|
tracing::debug!(?service_impl, "clear_expired_peer");
|
||||||
}
|
}
|
||||||
@@ -3180,6 +3287,7 @@ impl PeerRoute {
|
|||||||
session_mgr: RouteSessionManager,
|
session_mgr: RouteSessionManager,
|
||||||
) {
|
) {
|
||||||
let mut global_event_receiver = service_impl.global_ctx.subscribe();
|
let mut global_event_receiver = service_impl.global_ctx.subscribe();
|
||||||
|
service_impl.mark_interface_peers_dirty();
|
||||||
loop {
|
loop {
|
||||||
if service_impl.update_my_infos().await {
|
if service_impl.update_my_infos().await {
|
||||||
session_mgr.sync_now("update_my_infos");
|
session_mgr.sync_now("update_my_infos");
|
||||||
@@ -3193,6 +3301,12 @@ impl PeerRoute {
|
|||||||
|
|
||||||
select! {
|
select! {
|
||||||
ev = global_event_receiver.recv() => {
|
ev = global_event_receiver.recv() => {
|
||||||
|
if let Ok(ev_ref) = &ev {
|
||||||
|
service_impl.handle_global_ctx_event(ev_ref);
|
||||||
|
} else {
|
||||||
|
service_impl.mark_interface_peers_dirty();
|
||||||
|
global_event_receiver = global_event_receiver.resubscribe();
|
||||||
|
}
|
||||||
tracing::info!(?ev, "global event received in update_my_peer_info_routine");
|
tracing::info!(?ev, "global event received in update_my_peer_info_routine");
|
||||||
}
|
}
|
||||||
_ = tokio::time::sleep(Duration::from_secs(1)) => {}
|
_ = tokio::time::sleep(Duration::from_secs(1)) => {}
|
||||||
@@ -3448,18 +3562,25 @@ impl PeerPacketFilter for Arc<PeerRoute> {}
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{
|
use std::{
|
||||||
collections::BTreeSet,
|
collections::{BTreeSet, HashMap},
|
||||||
sync::{atomic::Ordering, Arc},
|
sync::{
|
||||||
time::Duration,
|
atomic::{AtomicU32, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
use cidr::{Ipv4Cidr, Ipv4Inet, Ipv6Inet};
|
use cidr::{Ipv4Cidr, Ipv4Inet, Ipv6Inet};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
|
use parking_lot::Mutex;
|
||||||
use prefix_trie::PrefixMap;
|
use prefix_trie::PrefixMap;
|
||||||
use prost_reflect::{DynamicMessage, ReflectMessage};
|
use prost_reflect::{DynamicMessage, ReflectMessage};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{global_ctx::tests::get_mock_global_ctx, PeerId},
|
common::{
|
||||||
|
global_ctx::{tests::get_mock_global_ctx, GlobalCtxEvent, TrustedKeySource},
|
||||||
|
PeerId,
|
||||||
|
},
|
||||||
connector::udp_hole_punch::tests::replace_stun_info_collector,
|
connector::udp_hole_punch::tests::replace_stun_info_collector,
|
||||||
peers::{
|
peers::{
|
||||||
create_packet_recv_chan,
|
create_packet_recv_chan,
|
||||||
@@ -3479,11 +3600,12 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
|
|
||||||
use super::PeerRoute;
|
use super::{PeerRoute, REMOVE_DEAD_PEER_INFO_AFTER};
|
||||||
|
|
||||||
struct AuthOnlyInterface {
|
struct AuthOnlyInterface {
|
||||||
my_peer_id: PeerId,
|
my_peer_id: PeerId,
|
||||||
identity_type: DashMap<PeerId, PeerIdentityType>,
|
identity_type: DashMap<PeerId, PeerIdentityType>,
|
||||||
|
peer_public_key: DashMap<PeerId, Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@@ -3496,11 +3618,183 @@ mod tests {
|
|||||||
self.my_peer_id
|
self.my_peer_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_peer_public_key(&self, peer_id: PeerId) -> Option<Vec<u8>> {
|
||||||
|
self.peer_public_key
|
||||||
|
.get(&peer_id)
|
||||||
|
.map(|x| x.value().clone())
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_peer_identity_type(&self, peer_id: PeerId) -> Option<PeerIdentityType> {
|
async fn get_peer_identity_type(&self, peer_id: PeerId) -> Option<PeerIdentityType> {
|
||||||
self.identity_type.get(&peer_id).map(|x| *x.value())
|
self.identity_type.get(&peer_id).map(|x| *x.value())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TrackingInterface {
|
||||||
|
my_peer_id: PeerId,
|
||||||
|
closed_peers: Arc<Mutex<Vec<PeerId>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl RouteInterface for TrackingInterface {
|
||||||
|
async fn list_peers(&self) -> Vec<PeerId> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn my_peer_id(&self) -> PeerId {
|
||||||
|
self.my_peer_id
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close_peer(&self, peer_id: PeerId) {
|
||||||
|
self.closed_peers.lock().push(peer_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CountingInterface {
|
||||||
|
my_peer_id: PeerId,
|
||||||
|
peers: Arc<Mutex<Vec<PeerId>>>,
|
||||||
|
peer_identity_types: Arc<Mutex<HashMap<PeerId, Option<PeerIdentityType>>>>,
|
||||||
|
list_peers_calls: Arc<AtomicU32>,
|
||||||
|
get_peer_identity_type_calls: Arc<AtomicU32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl RouteInterface for CountingInterface {
|
||||||
|
async fn list_peers(&self) -> Vec<PeerId> {
|
||||||
|
self.list_peers_calls.fetch_add(1, Ordering::Relaxed);
|
||||||
|
self.peers.lock().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_peer_identity_type(&self, peer_id: PeerId) -> Option<PeerIdentityType> {
|
||||||
|
self.get_peer_identity_type_calls
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
self.peer_identity_types
|
||||||
|
.lock()
|
||||||
|
.get(&peer_id)
|
||||||
|
.copied()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn my_peer_id(&self) -> PeerId {
|
||||||
|
self.my_peer_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn interface_peer_cache_refreshes_only_when_marked_dirty() {
|
||||||
|
let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx());
|
||||||
|
let peers = Arc::new(Mutex::new(vec![2, 3]));
|
||||||
|
let peer_identity_types = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
let list_peers_calls = Arc::new(AtomicU32::new(0));
|
||||||
|
let get_peer_identity_type_calls = Arc::new(AtomicU32::new(0));
|
||||||
|
*service_impl.interface.lock().await = Some(Box::new(CountingInterface {
|
||||||
|
my_peer_id: 1,
|
||||||
|
peers: peers.clone(),
|
||||||
|
peer_identity_types,
|
||||||
|
list_peers_calls: list_peers_calls.clone(),
|
||||||
|
get_peer_identity_type_calls,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let first: BTreeSet<_> = service_impl.list_peers_from_interface().await;
|
||||||
|
let second: BTreeSet<_> = service_impl.list_peers_from_interface().await;
|
||||||
|
|
||||||
|
assert_eq!(first, BTreeSet::from([2, 3]));
|
||||||
|
assert_eq!(second, BTreeSet::from([2, 3]));
|
||||||
|
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 1);
|
||||||
|
|
||||||
|
*peers.lock() = vec![2, 4];
|
||||||
|
service_impl.handle_global_ctx_event(&GlobalCtxEvent::PeerConnAdded(Default::default()));
|
||||||
|
|
||||||
|
let third: BTreeSet<_> = service_impl.list_peers_from_interface().await;
|
||||||
|
assert_eq!(third, BTreeSet::from([2, 4]));
|
||||||
|
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn update_my_conn_info_skips_interface_scan_when_topology_is_unchanged() {
|
||||||
|
let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx());
|
||||||
|
let peers = Arc::new(Mutex::new(vec![2, 3]));
|
||||||
|
let peer_identity_types = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
let list_peers_calls = Arc::new(AtomicU32::new(0));
|
||||||
|
let get_peer_identity_type_calls = Arc::new(AtomicU32::new(0));
|
||||||
|
*service_impl.interface.lock().await = Some(Box::new(CountingInterface {
|
||||||
|
my_peer_id: 1,
|
||||||
|
peers: peers.clone(),
|
||||||
|
peer_identity_types,
|
||||||
|
list_peers_calls: list_peers_calls.clone(),
|
||||||
|
get_peer_identity_type_calls: get_peer_identity_type_calls.clone(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert!(service_impl.update_my_conn_info().await);
|
||||||
|
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 1);
|
||||||
|
assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 2);
|
||||||
|
|
||||||
|
assert!(!service_impl.update_my_conn_info().await);
|
||||||
|
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 1);
|
||||||
|
assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 2);
|
||||||
|
|
||||||
|
*peers.lock() = vec![2, 4];
|
||||||
|
service_impl.handle_global_ctx_event(&GlobalCtxEvent::PeerConnRemoved(Default::default()));
|
||||||
|
|
||||||
|
assert!(service_impl.update_my_conn_info().await);
|
||||||
|
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 2);
|
||||||
|
assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 4);
|
||||||
|
|
||||||
|
assert!(!service_impl.update_my_conn_info().await);
|
||||||
|
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 2);
|
||||||
|
assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_peer_identity_type_reuses_snapshot_until_topology_changes() {
|
||||||
|
let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx());
|
||||||
|
let peers = Arc::new(Mutex::new(vec![2, 3]));
|
||||||
|
let peer_identity_types = Arc::new(Mutex::new(HashMap::from([
|
||||||
|
(2, Some(PeerIdentityType::Credential)),
|
||||||
|
(3, Some(PeerIdentityType::Admin)),
|
||||||
|
(4, Some(PeerIdentityType::Admin)),
|
||||||
|
])));
|
||||||
|
let list_peers_calls = Arc::new(AtomicU32::new(0));
|
||||||
|
let get_peer_identity_type_calls = Arc::new(AtomicU32::new(0));
|
||||||
|
*service_impl.interface.lock().await = Some(Box::new(CountingInterface {
|
||||||
|
my_peer_id: 1,
|
||||||
|
peers: peers.clone(),
|
||||||
|
peer_identity_types: peer_identity_types.clone(),
|
||||||
|
list_peers_calls: list_peers_calls.clone(),
|
||||||
|
get_peer_identity_type_calls: get_peer_identity_type_calls.clone(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
service_impl.get_peer_identity_type_from_interface(2).await,
|
||||||
|
Some(PeerIdentityType::Credential)
|
||||||
|
);
|
||||||
|
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 1);
|
||||||
|
assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 2);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
service_impl.get_peer_identity_type_from_interface(2).await,
|
||||||
|
Some(PeerIdentityType::Credential)
|
||||||
|
);
|
||||||
|
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 1);
|
||||||
|
assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 2);
|
||||||
|
|
||||||
|
*peers.lock() = vec![2, 4];
|
||||||
|
service_impl.handle_global_ctx_event(&GlobalCtxEvent::PeerConnRemoved(Default::default()));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
service_impl.get_peer_identity_type_from_interface(4).await,
|
||||||
|
Some(PeerIdentityType::Admin)
|
||||||
|
);
|
||||||
|
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 2);
|
||||||
|
assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 4);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
service_impl.get_peer_identity_type_from_interface(4).await,
|
||||||
|
Some(PeerIdentityType::Admin)
|
||||||
|
);
|
||||||
|
assert_eq!(list_peers_calls.load(Ordering::Relaxed), 2);
|
||||||
|
assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 4);
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_mock_route(peer_mgr: Arc<PeerManager>) -> Arc<PeerRoute> {
|
async fn create_mock_route(peer_mgr: Arc<PeerManager>) -> Arc<PeerRoute> {
|
||||||
let peer_route = PeerRoute::new(
|
let peer_route = PeerRoute::new(
|
||||||
peer_mgr.my_peer_id(),
|
peer_mgr.my_peer_id(),
|
||||||
@@ -3654,13 +3948,29 @@ mod tests {
|
|||||||
let route = create_mock_route(peer_mgr.clone()).await;
|
let route = create_mock_route(peer_mgr.clone()).await;
|
||||||
let from_peer_id: PeerId = 10001;
|
let from_peer_id: PeerId = 10001;
|
||||||
let forwarded_peer_id: PeerId = 10002;
|
let forwarded_peer_id: PeerId = 10002;
|
||||||
|
let credential_pubkey = vec![3u8; 32];
|
||||||
|
|
||||||
let identity_type = DashMap::new();
|
let identity_type = DashMap::new();
|
||||||
identity_type.insert(from_peer_id, PeerIdentityType::Credential);
|
identity_type.insert(from_peer_id, PeerIdentityType::Credential);
|
||||||
|
let peer_public_key = DashMap::new();
|
||||||
|
peer_public_key.insert(from_peer_id, credential_pubkey.clone());
|
||||||
*route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface {
|
*route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface {
|
||||||
my_peer_id: peer_mgr.my_peer_id(),
|
my_peer_id: peer_mgr.my_peer_id(),
|
||||||
identity_type,
|
identity_type,
|
||||||
|
peer_public_key,
|
||||||
}));
|
}));
|
||||||
|
route
|
||||||
|
.service_impl
|
||||||
|
.synced_route_info
|
||||||
|
.trusted_credential_pubkeys
|
||||||
|
.insert(
|
||||||
|
credential_pubkey.clone(),
|
||||||
|
TrustedCredentialPubkey {
|
||||||
|
pubkey: credential_pubkey,
|
||||||
|
expiry_unix: i64::MAX,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let mut sender_info = RoutePeerInfo::new();
|
let mut sender_info = RoutePeerInfo::new();
|
||||||
sender_info.peer_id = from_peer_id;
|
sender_info.peer_id = from_peer_id;
|
||||||
@@ -3703,6 +4013,7 @@ mod tests {
|
|||||||
assert!(guard.get(&forwarded_peer_id).is_none());
|
assert!(guard.get(&forwarded_peer_id).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shared node doesn't have hmac.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn sync_route_info_shared_sender_cannot_publish_trusted_credentials() {
|
async fn sync_route_info_shared_sender_cannot_publish_trusted_credentials() {
|
||||||
let peer_mgr = create_mock_pmgr().await;
|
let peer_mgr = create_mock_pmgr().await;
|
||||||
@@ -3716,6 +4027,7 @@ mod tests {
|
|||||||
*route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface {
|
*route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface {
|
||||||
my_peer_id: peer_mgr.my_peer_id(),
|
my_peer_id: peer_mgr.my_peer_id(),
|
||||||
identity_type,
|
identity_type,
|
||||||
|
peer_public_key: DashMap::new(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let mut sender_info = RoutePeerInfo::new();
|
let mut sender_info = RoutePeerInfo::new();
|
||||||
@@ -3755,13 +4067,6 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let guard = route.service_impl.synced_route_info.peer_infos.read();
|
|
||||||
assert!(guard
|
|
||||||
.get(&forwarded_peer_id)
|
|
||||||
.map(|x| x.trusted_credential_pubkeys.is_empty())
|
|
||||||
.unwrap_or(false));
|
|
||||||
drop(guard);
|
|
||||||
|
|
||||||
assert!(!route
|
assert!(!route
|
||||||
.service_impl
|
.service_impl
|
||||||
.synced_route_info
|
.synced_route_info
|
||||||
@@ -3770,60 +4075,83 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn sync_route_info_forces_non_credential_for_legacy_admin_sender() {
|
async fn clear_expired_peer_recomputes_trust_after_last_admin_disappears() {
|
||||||
let peer_mgr = create_mock_pmgr().await;
|
let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx());
|
||||||
let route = create_mock_route(peer_mgr.clone()).await;
|
let admin_peer_id: PeerId = 10051;
|
||||||
let from_peer_id: PeerId = 10011;
|
let credential_peer_id: PeerId = 10052;
|
||||||
let other_peer_id: PeerId = 10012;
|
let admin_pubkey = vec![5u8; 32];
|
||||||
|
let credential_pubkey = vec![6u8; 32];
|
||||||
|
let network_name = service_impl
|
||||||
|
.global_ctx
|
||||||
|
.get_network_identity()
|
||||||
|
.network_name
|
||||||
|
.clone();
|
||||||
|
let now = SystemTime::now();
|
||||||
|
let closed_peers = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
|
||||||
let identity_type = DashMap::new();
|
*service_impl.interface.lock().await = Some(Box::new(TrackingInterface {
|
||||||
identity_type.insert(from_peer_id, PeerIdentityType::Admin);
|
my_peer_id: service_impl.my_peer_id,
|
||||||
*route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface {
|
closed_peers: closed_peers.clone(),
|
||||||
my_peer_id: peer_mgr.my_peer_id(),
|
|
||||||
identity_type,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let mut sender_info = RoutePeerInfo::new();
|
{
|
||||||
sender_info.peer_id = from_peer_id;
|
let mut guard = service_impl.synced_route_info.peer_infos.write();
|
||||||
sender_info.version = 1;
|
|
||||||
sender_info.feature_flag = Some(PeerFeatureFlag {
|
let mut admin_info = RoutePeerInfo::new();
|
||||||
is_credential_peer: true,
|
admin_info.peer_id = admin_peer_id;
|
||||||
|
admin_info.version = 1;
|
||||||
|
admin_info.last_update =
|
||||||
|
Some((now - REMOVE_DEAD_PEER_INFO_AFTER - Duration::from_secs(1)).into());
|
||||||
|
admin_info.noise_static_pubkey = admin_pubkey;
|
||||||
|
admin_info.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof {
|
||||||
|
credential: Some(TrustedCredentialPubkey {
|
||||||
|
pubkey: credential_pubkey.clone(),
|
||||||
|
expiry_unix: i64::MAX,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
}),
|
||||||
|
credential_hmac: vec![1; 32],
|
||||||
|
}];
|
||||||
|
|
||||||
let mut other_info = RoutePeerInfo::new();
|
let mut credential_info = RoutePeerInfo::new();
|
||||||
other_info.peer_id = other_peer_id;
|
credential_info.peer_id = credential_peer_id;
|
||||||
other_info.version = 1;
|
credential_info.version = 1;
|
||||||
|
credential_info.last_update = Some(now.into());
|
||||||
|
credential_info.noise_static_pubkey = credential_pubkey.clone();
|
||||||
|
|
||||||
let make_raw = |info: &RoutePeerInfo| {
|
guard.insert(admin_peer_id, admin_info);
|
||||||
let mut raw = DynamicMessage::new(RoutePeerInfo::default().descriptor());
|
guard.insert(credential_peer_id, credential_info);
|
||||||
raw.transcode_from(info).unwrap();
|
}
|
||||||
raw
|
|
||||||
};
|
|
||||||
let raw_infos = vec![make_raw(&sender_info), make_raw(&other_info)];
|
|
||||||
|
|
||||||
route
|
let (_, global_trusted_keys) = service_impl
|
||||||
.session_mgr
|
.synced_route_info
|
||||||
.do_sync_route_info(
|
.verify_and_update_credential_trusts(None);
|
||||||
from_peer_id,
|
service_impl
|
||||||
1,
|
.global_ctx
|
||||||
true,
|
.update_trusted_keys(global_trusted_keys, &network_name);
|
||||||
Some(vec![sender_info, other_info]),
|
|
||||||
Some(raw_infos),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let guard = route.service_impl.synced_route_info.peer_infos.read();
|
assert!(service_impl
|
||||||
let sender = guard.get(&from_peer_id).unwrap();
|
.synced_route_info
|
||||||
assert!(!sender
|
.trusted_credential_pubkeys
|
||||||
.feature_flag
|
.contains_key(&credential_pubkey));
|
||||||
.as_ref()
|
|
||||||
.map(|x| x.is_credential_peer)
|
service_impl.clear_expired_peer().await;
|
||||||
.unwrap_or(false));
|
|
||||||
assert!(guard.get(&other_peer_id).is_some());
|
assert!(!service_impl.global_ctx.is_pubkey_trusted_with_source(
|
||||||
|
&credential_pubkey,
|
||||||
|
&network_name,
|
||||||
|
TrustedKeySource::OspfCredential,
|
||||||
|
));
|
||||||
|
assert!(closed_peers.lock().contains(&credential_peer_id));
|
||||||
|
assert!(!service_impl
|
||||||
|
.synced_route_info
|
||||||
|
.peer_infos
|
||||||
|
.read()
|
||||||
|
.contains_key(&admin_peer_id));
|
||||||
|
assert!(!service_impl
|
||||||
|
.synced_route_info
|
||||||
|
.peer_infos
|
||||||
|
.read()
|
||||||
|
.contains_key(&credential_peer_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest::rstest]
|
#[rstest::rstest]
|
||||||
@@ -4453,13 +4781,29 @@ mod tests {
|
|||||||
let peer_mgr = create_mock_pmgr().await;
|
let peer_mgr = create_mock_pmgr().await;
|
||||||
let route = create_mock_route(peer_mgr.clone()).await;
|
let route = create_mock_route(peer_mgr.clone()).await;
|
||||||
let from_peer_id: PeerId = 20001;
|
let from_peer_id: PeerId = 20001;
|
||||||
|
let credential_pubkey = vec![4u8; 32];
|
||||||
|
|
||||||
let identity_type = DashMap::new();
|
let identity_type = DashMap::new();
|
||||||
identity_type.insert(from_peer_id, PeerIdentityType::Credential);
|
identity_type.insert(from_peer_id, PeerIdentityType::Credential);
|
||||||
|
let peer_public_key = DashMap::new();
|
||||||
|
peer_public_key.insert(from_peer_id, credential_pubkey.clone());
|
||||||
*route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface {
|
*route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface {
|
||||||
my_peer_id: peer_mgr.my_peer_id(),
|
my_peer_id: peer_mgr.my_peer_id(),
|
||||||
identity_type,
|
identity_type,
|
||||||
|
peer_public_key,
|
||||||
}));
|
}));
|
||||||
|
route
|
||||||
|
.service_impl
|
||||||
|
.synced_route_info
|
||||||
|
.trusted_credential_pubkeys
|
||||||
|
.insert(
|
||||||
|
credential_pubkey.clone(),
|
||||||
|
TrustedCredentialPubkey {
|
||||||
|
pubkey: credential_pubkey,
|
||||||
|
expiry_unix: i64::MAX,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let mut sender_info = RoutePeerInfo::new();
|
let mut sender_info = RoutePeerInfo::new();
|
||||||
sender_info.peer_id = from_peer_id;
|
sender_info.peer_id = from_peer_id;
|
||||||
@@ -4505,6 +4849,7 @@ mod tests {
|
|||||||
*route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface {
|
*route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface {
|
||||||
my_peer_id: peer_mgr.my_peer_id(),
|
my_peer_id: peer_mgr.my_peer_id(),
|
||||||
identity_type,
|
identity_type,
|
||||||
|
peer_public_key: DashMap::new(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let mut sender_info = RoutePeerInfo::new();
|
let mut sender_info = RoutePeerInfo::new();
|
||||||
@@ -4575,6 +4920,7 @@ mod tests {
|
|||||||
*route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface {
|
*route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface {
|
||||||
my_peer_id: peer_mgr.my_peer_id(),
|
my_peer_id: peer_mgr.my_peer_id(),
|
||||||
identity_type,
|
identity_type,
|
||||||
|
peer_public_key: DashMap::new(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let mut sender_info = RoutePeerInfo::new();
|
let mut sender_info = RoutePeerInfo::new();
|
||||||
|
|||||||
@@ -212,7 +212,12 @@ impl PeerSessionStore {
|
|||||||
.clone();
|
.clone();
|
||||||
session.check_encrypt_algo_same(&send_algorithm, &recv_algorithm)?;
|
session.check_encrypt_algo_same(&send_algorithm, &recv_algorithm)?;
|
||||||
session.check_or_set_peer_static_pubkey(peer_static_pubkey)?;
|
session.check_or_set_peer_static_pubkey(peer_static_pubkey)?;
|
||||||
session.sync_root_key(root_key, b_session_generation, initial_epoch);
|
session.sync_root_key(
|
||||||
|
root_key,
|
||||||
|
b_session_generation,
|
||||||
|
initial_epoch,
|
||||||
|
matches!(action, PeerSessionAction::Sync),
|
||||||
|
);
|
||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,6 +357,33 @@ impl EpochRxSlot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
struct SyncRxGrace {
|
||||||
|
slots: [[EpochRxSlot; 2]; 2],
|
||||||
|
expires_at_ms: u64,
|
||||||
|
valid: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyncRxGrace {
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.slots = [[EpochRxSlot::default(), EpochRxSlot::default()]; 2];
|
||||||
|
self.expires_at_ms = 0;
|
||||||
|
self.valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh(&mut self, slots: [[EpochRxSlot; 2]; 2], expires_at_ms: u64) {
|
||||||
|
self.slots = slots;
|
||||||
|
self.expires_at_ms = expires_at_ms;
|
||||||
|
self.valid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_expire(&mut self, now_ms: u64) {
|
||||||
|
if self.valid && now_ms >= self.expires_at_ms {
|
||||||
|
self.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PeerSession {
|
pub struct PeerSession {
|
||||||
peer_id: PeerId,
|
peer_id: PeerId,
|
||||||
root_key: RwLock<[u8; 32]>,
|
root_key: RwLock<[u8; 32]>,
|
||||||
@@ -365,6 +397,8 @@ pub struct PeerSession {
|
|||||||
|
|
||||||
rx_slots: Mutex<[[EpochRxSlot; 2]; 2]>,
|
rx_slots: Mutex<[[EpochRxSlot; 2]; 2]>,
|
||||||
key_cache: Mutex<[[EpochKeySlot; 2]; 2]>,
|
key_cache: Mutex<[[EpochKeySlot; 2]; 2]>,
|
||||||
|
sync_rx_grace: Mutex<SyncRxGrace>,
|
||||||
|
sync_rx_grace_expires_at_ms: AtomicU64,
|
||||||
|
|
||||||
send_cipher_algorithm: String,
|
send_cipher_algorithm: String,
|
||||||
recv_cipher_algorithm: String,
|
recv_cipher_algorithm: String,
|
||||||
@@ -389,6 +423,11 @@ impl std::fmt::Debug for PeerSession {
|
|||||||
.field("send_packets_since_epoch", &self.send_packets_since_epoch)
|
.field("send_packets_since_epoch", &self.send_packets_since_epoch)
|
||||||
.field("rx_slots", &self.rx_slots)
|
.field("rx_slots", &self.rx_slots)
|
||||||
.field("key_cache", &self.key_cache)
|
.field("key_cache", &self.key_cache)
|
||||||
|
.field("sync_rx_grace", &self.sync_rx_grace)
|
||||||
|
.field(
|
||||||
|
"sync_rx_grace_expires_at_ms",
|
||||||
|
&self.sync_rx_grace_expires_at_ms,
|
||||||
|
)
|
||||||
.field("send_cipher_algorithm", &self.send_cipher_algorithm)
|
.field("send_cipher_algorithm", &self.send_cipher_algorithm)
|
||||||
.field("recv_cipher_algorithm", &self.recv_cipher_algorithm)
|
.field("recv_cipher_algorithm", &self.recv_cipher_algorithm)
|
||||||
.finish()
|
.finish()
|
||||||
@@ -405,6 +444,10 @@ impl PeerSession {
|
|||||||
/// traffic may want to increase this value; low-latency or tightly
|
/// traffic may want to increase this value; low-latency or tightly
|
||||||
/// resource-constrained deployments may lower it.
|
/// resource-constrained deployments may lower it.
|
||||||
const EVICT_IDLE_AFTER_MS: u64 = 30_000;
|
const EVICT_IDLE_AFTER_MS: u64 = 30_000;
|
||||||
|
/// Keep the pre-sync receive windows alive briefly so in-flight packets
|
||||||
|
/// from the previous epochs are still accepted after a shared session is
|
||||||
|
/// synced in place by another connection.
|
||||||
|
const SYNC_RX_GRACE_AFTER_MS: u64 = 5_000;
|
||||||
|
|
||||||
/// Maximum number of packets to send in a single epoch before forcing
|
/// Maximum number of packets to send in a single epoch before forcing
|
||||||
/// a key/epoch rotation.
|
/// a key/epoch rotation.
|
||||||
@@ -458,6 +501,8 @@ impl PeerSession {
|
|||||||
send_packets_since_epoch: AtomicU64::new(0),
|
send_packets_since_epoch: AtomicU64::new(0),
|
||||||
rx_slots: Mutex::new(rx_slots),
|
rx_slots: Mutex::new(rx_slots),
|
||||||
key_cache: Mutex::new(key_cache),
|
key_cache: Mutex::new(key_cache),
|
||||||
|
sync_rx_grace: Mutex::new(SyncRxGrace::default()),
|
||||||
|
sync_rx_grace_expires_at_ms: AtomicU64::new(0),
|
||||||
send_cipher_algorithm,
|
send_cipher_algorithm,
|
||||||
recv_cipher_algorithm,
|
recv_cipher_algorithm,
|
||||||
invalidated: AtomicBool::new(false),
|
invalidated: AtomicBool::new(false),
|
||||||
@@ -540,7 +585,15 @@ impl PeerSession {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync_root_key(&self, root_key: [u8; 32], session_generation: u32, initial_epoch: u32) {
|
pub fn sync_root_key(
|
||||||
|
&self,
|
||||||
|
root_key: [u8; 32],
|
||||||
|
session_generation: u32,
|
||||||
|
initial_epoch: u32,
|
||||||
|
preserve_rx_grace: bool,
|
||||||
|
) {
|
||||||
|
let old_root_key = self.root_key();
|
||||||
|
let can_preserve_rx_grace = preserve_rx_grace && old_root_key == root_key;
|
||||||
{
|
{
|
||||||
let mut g = self.root_key.write().unwrap();
|
let mut g = self.root_key.write().unwrap();
|
||||||
*g = root_key;
|
*g = root_key;
|
||||||
@@ -557,6 +610,16 @@ impl PeerSession {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let mut rx = self.rx_slots.lock().unwrap();
|
let mut rx = self.rx_slots.lock().unwrap();
|
||||||
|
let mut sync_rx_grace = self.sync_rx_grace.lock().unwrap();
|
||||||
|
if can_preserve_rx_grace {
|
||||||
|
let expires_at_ms = now_ms().saturating_add(Self::SYNC_RX_GRACE_AFTER_MS);
|
||||||
|
sync_rx_grace.refresh(*rx, expires_at_ms);
|
||||||
|
self.sync_rx_grace_expires_at_ms
|
||||||
|
.store(expires_at_ms, Ordering::Relaxed);
|
||||||
|
} else {
|
||||||
|
sync_rx_grace.clear();
|
||||||
|
self.sync_rx_grace_expires_at_ms.store(0, Ordering::Relaxed);
|
||||||
|
}
|
||||||
for dir in 0..2 {
|
for dir in 0..2 {
|
||||||
rx[dir][0].clear();
|
rx[dir][0].clear();
|
||||||
rx[dir][1].clear();
|
rx[dir][1].clear();
|
||||||
@@ -598,21 +661,17 @@ impl PeerSession {
|
|||||||
key
|
key
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_encryptor(&self, epoch: u32, dir: usize, is_send: bool) -> Option<Arc<dyn Encryptor>> {
|
fn get_or_create_encryptor(
|
||||||
let generation = self.session_generation();
|
&self,
|
||||||
let rx = self.rx_slots.lock().unwrap();
|
epoch: u32,
|
||||||
let send_epoch = self.send_epoch.load(Ordering::Relaxed);
|
dir: usize,
|
||||||
let allowed = epoch == send_epoch
|
generation: u32,
|
||||||
|| rx[dir][0].valid && rx[dir][0].epoch == epoch
|
is_send: bool,
|
||||||
|| rx[dir][1].valid && rx[dir][1].epoch == epoch;
|
) -> Arc<dyn Encryptor> {
|
||||||
if !allowed {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut guard = self.key_cache.lock().unwrap();
|
let mut guard = self.key_cache.lock().unwrap();
|
||||||
for slot in guard[dir].iter_mut() {
|
for slot in guard[dir].iter_mut() {
|
||||||
if slot.valid && slot.epoch == epoch && slot.generation == generation {
|
if slot.valid && slot.epoch == epoch && slot.generation == generation {
|
||||||
return Some(slot.get_encryptor(is_send));
|
return slot.get_encryptor(is_send);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,7 +694,7 @@ impl PeerSession {
|
|||||||
guard[dir][1] = slot;
|
guard[dir][1] = slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(ret)
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maybe_rotate_epoch(&self, now_ms: u64) {
|
fn maybe_rotate_epoch(&self, now_ms: u64) {
|
||||||
@@ -698,9 +757,38 @@ impl PeerSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn epoch_in_slots(slots: &[EpochRxSlot; 2], epoch: u32) -> bool {
|
||||||
|
slots[0].valid && slots[0].epoch == epoch || slots[1].valid && slots[1].epoch == epoch
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_rx_grace_active(&self, now_ms: u64) -> bool {
|
||||||
|
let expires_at_ms = self.sync_rx_grace_expires_at_ms.load(Ordering::Relaxed);
|
||||||
|
if expires_at_ms == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if now_ms < expires_at_ms {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
self.sync_rx_grace_expires_at_ms.store(0, Ordering::Relaxed);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn check_replay(&self, epoch: u32, seq: u64, dir: usize, now_ms: u64) -> bool {
|
fn check_replay(&self, epoch: u32, seq: u64, dir: usize, now_ms: u64) -> bool {
|
||||||
let mut rx = self.rx_slots.lock().unwrap();
|
let mut rx = self.rx_slots.lock().unwrap();
|
||||||
Self::evict_old_rx_slots(&mut rx, now_ms);
|
Self::evict_old_rx_slots(&mut rx, now_ms);
|
||||||
|
let mut sync_rx_grace = if self.sync_rx_grace_active(now_ms) {
|
||||||
|
let mut sync_rx_grace = self.sync_rx_grace.lock().unwrap();
|
||||||
|
sync_rx_grace.maybe_expire(now_ms);
|
||||||
|
if sync_rx_grace.valid {
|
||||||
|
Self::evict_old_rx_slots(&mut sync_rx_grace.slots, now_ms);
|
||||||
|
Some(sync_rx_grace)
|
||||||
|
} else {
|
||||||
|
self.sync_rx_grace_expires_at_ms.store(0, Ordering::Relaxed);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let send_epoch = self.send_epoch.load(Ordering::Relaxed);
|
let send_epoch = self.send_epoch.load(Ordering::Relaxed);
|
||||||
{
|
{
|
||||||
let mut key_cache = self.key_cache.lock().unwrap();
|
let mut key_cache = self.key_cache.lock().unwrap();
|
||||||
@@ -712,7 +800,10 @@ impl PeerSession {
|
|||||||
let e = key_cache[d][s].epoch;
|
let e = key_cache[d][s].epoch;
|
||||||
let allowed = e == send_epoch
|
let allowed = e == send_epoch
|
||||||
|| rx[d][0].valid && rx[d][0].epoch == e
|
|| rx[d][0].valid && rx[d][0].epoch == e
|
||||||
|| rx[d][1].valid && rx[d][1].epoch == e;
|
|| rx[d][1].valid && rx[d][1].epoch == e
|
||||||
|
|| sync_rx_grace
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|g| Self::epoch_in_slots(&g.slots[d], e));
|
||||||
if !allowed {
|
if !allowed {
|
||||||
key_cache[d][s].valid = false;
|
key_cache[d][s].valid = false;
|
||||||
}
|
}
|
||||||
@@ -720,6 +811,18 @@ impl PeerSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sync_rx_grace
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|g| Self::epoch_in_slots(&g.slots[dir], epoch))
|
||||||
|
{
|
||||||
|
for slot in sync_rx_grace.as_mut().unwrap().slots[dir].iter_mut() {
|
||||||
|
if slot.valid && slot.epoch == epoch {
|
||||||
|
slot.last_rx_ms = now_ms;
|
||||||
|
return slot.window.accept(seq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !rx[dir][0].valid {
|
if !rx[dir][0].valid {
|
||||||
rx[dir][0] = EpochRxSlot {
|
rx[dir][0] = EpochRxSlot {
|
||||||
epoch,
|
epoch,
|
||||||
@@ -777,9 +880,7 @@ impl PeerSession {
|
|||||||
}
|
}
|
||||||
let dir = Self::dir_for_sender(sender_peer_id, receiver_peer_id);
|
let dir = Self::dir_for_sender(sender_peer_id, receiver_peer_id);
|
||||||
let (epoch, _seq, nonce_bytes) = self.next_nonce(dir);
|
let (epoch, _seq, nonce_bytes) = self.next_nonce(dir);
|
||||||
let encryptor = self
|
let encryptor = self.get_or_create_encryptor(epoch, dir, self.session_generation(), true);
|
||||||
.get_encryptor(epoch, dir, true)
|
|
||||||
.ok_or_else(|| anyhow!("no key for epoch"))?;
|
|
||||||
if let Err(e) = encryptor.encrypt_with_nonce(pkt, Some(nonce_bytes.as_slice())) {
|
if let Err(e) = encryptor.encrypt_with_nonce(pkt, Some(nonce_bytes.as_slice())) {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
peer_id = ?self.peer_id,
|
peer_id = ?self.peer_id,
|
||||||
@@ -816,9 +917,7 @@ impl PeerSession {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let encryptor = self
|
let encryptor = self.get_or_create_encryptor(epoch, dir, self.session_generation(), false);
|
||||||
.get_encryptor(epoch, dir, false)
|
|
||||||
.ok_or_else(|| anyhow!("no key for epoch"))?;
|
|
||||||
if let Err(e) = encryptor.decrypt(ciphertext_with_tail) {
|
if let Err(e) = encryptor.decrypt(ciphertext_with_tail) {
|
||||||
let count = self.decrypt_fail_count.fetch_add(1, Ordering::Relaxed) + 1;
|
let count = self.decrypt_fail_count.fetch_add(1, Ordering::Relaxed) + 1;
|
||||||
if count >= Self::DECRYPT_FAIL_THRESHOLD {
|
if count >= Self::DECRYPT_FAIL_THRESHOLD {
|
||||||
@@ -974,7 +1073,7 @@ mod tests {
|
|||||||
assert!(s.check_replay(0, 1, 0, now));
|
assert!(s.check_replay(0, 1, 0, now));
|
||||||
|
|
||||||
// Sync with initial_epoch=2 (simulating a Sync action)
|
// Sync with initial_epoch=2 (simulating a Sync action)
|
||||||
s.sync_root_key(root_key, 2, 2);
|
s.sync_root_key(root_key, 2, 2, true);
|
||||||
|
|
||||||
// Remote peer is still sending at epoch 0 — should be accepted
|
// Remote peer is still sending at epoch 0 — should be accepted
|
||||||
// (rx_slots were cleared, so the first packet establishes the epoch)
|
// (rx_slots were cleared, so the first packet establishes the epoch)
|
||||||
@@ -983,4 +1082,85 @@ mod tests {
|
|||||||
"packets at old epoch should be accepted after sync"
|
"packets at old epoch should be accepted after sync"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_root_key_keeps_previous_epochs_during_grace_window() {
|
||||||
|
let peer_id: PeerId = 10;
|
||||||
|
let root_key = PeerSession::new_root_key();
|
||||||
|
let s = PeerSession::new(
|
||||||
|
peer_id,
|
||||||
|
root_key,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
"aes-256-gcm".to_string(),
|
||||||
|
"aes-256-gcm".to_string(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let now = now_ms();
|
||||||
|
assert!(s.check_replay(0, 0, 0, now));
|
||||||
|
assert!(s.check_replay(1, 0, 0, now + 1));
|
||||||
|
|
||||||
|
s.sync_root_key(root_key, 2, 2, true);
|
||||||
|
|
||||||
|
// The first packet after sync may already use the new epoch.
|
||||||
|
assert!(s.check_replay(2, 0, 0, now + 2));
|
||||||
|
// Older in-flight packets from pre-sync epochs should still be accepted
|
||||||
|
// during the grace period, regardless of arrival order.
|
||||||
|
assert!(s.check_replay(1, 1, 0, now + 3));
|
||||||
|
assert!(s.check_replay(0, 1, 0, now + 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_root_key_expires_previous_epochs_after_grace_window() {
|
||||||
|
let peer_id: PeerId = 10;
|
||||||
|
let root_key = PeerSession::new_root_key();
|
||||||
|
let s = PeerSession::new(
|
||||||
|
peer_id,
|
||||||
|
root_key,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
"aes-256-gcm".to_string(),
|
||||||
|
"aes-256-gcm".to_string(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let now = now_ms();
|
||||||
|
assert!(s.check_replay(0, 0, 0, now));
|
||||||
|
assert!(s.check_replay(1, 0, 0, now + 1));
|
||||||
|
|
||||||
|
s.sync_root_key(root_key, 2, 2, true);
|
||||||
|
assert!(s.check_replay(2, 0, 0, now + 2));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!s.check_replay(0, 1, 0, now + PeerSession::SYNC_RX_GRACE_AFTER_MS + 3),
|
||||||
|
"old epochs should stop being accepted once the sync grace window expires"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_root_key_does_not_preserve_previous_epochs_when_root_key_changes() {
|
||||||
|
let peer_id: PeerId = 10;
|
||||||
|
let root_key = PeerSession::new_root_key();
|
||||||
|
let s = PeerSession::new(
|
||||||
|
peer_id,
|
||||||
|
root_key,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
"aes-256-gcm".to_string(),
|
||||||
|
"aes-256-gcm".to_string(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let now = now_ms();
|
||||||
|
assert!(s.check_replay(0, 0, 0, now));
|
||||||
|
assert!(s.check_replay(1, 0, 0, now + 1));
|
||||||
|
|
||||||
|
s.sync_root_key(PeerSession::new_root_key(), 2, 2, true);
|
||||||
|
assert!(s.check_replay(2, 0, 0, now + 2));
|
||||||
|
assert!(
|
||||||
|
!s.check_replay(1, 1, 0, now + 3),
|
||||||
|
"old epochs should not be preserved when sync replaces the root key"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,13 @@ pub type ForeignNetworkRouteInfoMap =
|
|||||||
pub trait RouteInterface {
|
pub trait RouteInterface {
|
||||||
async fn list_peers(&self) -> Vec<PeerId>;
|
async fn list_peers(&self) -> Vec<PeerId>;
|
||||||
fn my_peer_id(&self) -> PeerId;
|
fn my_peer_id(&self) -> PeerId;
|
||||||
|
fn need_periodic_requery_peers(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
async fn close_peer(&self, _peer_id: PeerId) {}
|
async fn close_peer(&self, _peer_id: PeerId) {}
|
||||||
|
async fn get_peer_public_key(&self, _peer_id: PeerId) -> Option<Vec<u8>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
async fn get_peer_identity_type(&self, _peer_id: PeerId) -> Option<PeerIdentityType> {
|
async fn get_peer_identity_type(&self, _peer_id: PeerId) -> Option<PeerIdentityType> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ import "common.proto";
|
|||||||
|
|
||||||
package web;
|
package web;
|
||||||
|
|
||||||
|
message DeviceOsInfo {
|
||||||
|
string os_type = 1;
|
||||||
|
string version = 2;
|
||||||
|
string distribution = 3;
|
||||||
|
}
|
||||||
|
|
||||||
message HeartbeatRequest {
|
message HeartbeatRequest {
|
||||||
common.UUID machine_id = 1;
|
common.UUID machine_id = 1;
|
||||||
common.UUID inst_id = 2;
|
common.UUID inst_id = 2;
|
||||||
@@ -14,6 +20,7 @@ message HeartbeatRequest {
|
|||||||
string hostname = 6;
|
string hostname = 6;
|
||||||
|
|
||||||
repeated common.UUID running_network_instances = 7;
|
repeated common.UUID running_network_instances = 7;
|
||||||
|
DeviceOsInfo device_os = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message HeartbeatResponse {}
|
message HeartbeatResponse {}
|
||||||
|
|||||||
@@ -57,6 +57,17 @@ fn get_faketcp_tunnel_type_str(driver_type: &str) -> String {
|
|||||||
format!("faketcp_{}", driver_type)
|
format!("faketcp_{}", driver_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_tun_off_runtime(
|
||||||
|
interface_name: String,
|
||||||
|
src_addr: Option<SocketAddr>,
|
||||||
|
dst_addr: SocketAddr,
|
||||||
|
) -> Result<Arc<dyn stack::Tun>, TunnelError> {
|
||||||
|
tokio::task::spawn_blocking(move || create_tun(&interface_name, src_addr, dst_addr))
|
||||||
|
.await
|
||||||
|
.map_err(|e| TunnelError::InternalError(format!("faketcp create_tun task failed: {e}")))?
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
pub struct FakeTcpTunnelListener {
|
pub struct FakeTcpTunnelListener {
|
||||||
addr: url::Url,
|
addr: url::Url,
|
||||||
os_listener: Option<tokio::net::TcpListener>,
|
os_listener: Option<tokio::net::TcpListener>,
|
||||||
@@ -137,7 +148,9 @@ impl FakeTcpTunnelListener {
|
|||||||
let ret = match self.stack_map.entry(interface_name.to_string()) {
|
let ret = match self.stack_map.entry(interface_name.to_string()) {
|
||||||
dashmap::Entry::Occupied(entry) => entry.get().clone(),
|
dashmap::Entry::Occupied(entry) => entry.get().clone(),
|
||||||
dashmap::Entry::Vacant(entry) => {
|
dashmap::Entry::Vacant(entry) => {
|
||||||
let tun = create_tun(interface_name, None, local_socket_addr)?;
|
let tun =
|
||||||
|
create_tun_off_runtime(interface_name.to_string(), None, local_socket_addr)
|
||||||
|
.await?;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
?local_socket_addr,
|
?local_socket_addr,
|
||||||
"create new stack with interface_name: {:?}",
|
"create new stack with interface_name: {:?}",
|
||||||
@@ -314,7 +327,8 @@ impl crate::tunnel::TunnelConnector for FakeTcpTunnelConnector {
|
|||||||
IpAddr::V6(ip) => (None, Some(ip)),
|
IpAddr::V6(ip) => (None, Some(ip)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let tun = create_tun(&interface_name, Some(remote_addr), local_addr)?;
|
let tun =
|
||||||
|
create_tun_off_runtime(interface_name.clone(), Some(remote_addr), local_addr).await?;
|
||||||
let local_ip = local_ip.unwrap_or("0.0.0.0".parse().unwrap());
|
let local_ip = local_ip.unwrap_or("0.0.0.0".parse().unwrap());
|
||||||
let mut stack = stack::Stack::new(tun, local_ip, local_ip6, mac);
|
let mut stack = stack::Stack::new(tun, local_ip, local_ip6, mac);
|
||||||
let driver_type = stack.driver_type();
|
let driver_type = stack.driver_type();
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ fn read_packet_socket_stats(fd: i32) -> io::Result<PacketSocketStats> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct LinuxBpfTun {
|
pub struct LinuxBpfTun {
|
||||||
fd: OwnedFd,
|
fd: Arc<OwnedFd>,
|
||||||
ifindex: i32,
|
ifindex: i32,
|
||||||
stop: Arc<AtomicBool>,
|
stop: Arc<AtomicBool>,
|
||||||
worker: Option<std::thread::JoinHandle<()>>,
|
worker: Option<std::thread::JoinHandle<()>>,
|
||||||
@@ -395,7 +395,7 @@ impl LinuxBpfTun {
|
|||||||
if fd < 0 {
|
if fd < 0 {
|
||||||
return Err(io::Error::last_os_error());
|
return Err(io::Error::last_os_error());
|
||||||
}
|
}
|
||||||
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
|
let fd = Arc::new(unsafe { OwnedFd::from_raw_fd(fd) });
|
||||||
|
|
||||||
let mut addr: libc::sockaddr_ll = unsafe { mem::zeroed() };
|
let mut addr: libc::sockaddr_ll = unsafe { mem::zeroed() };
|
||||||
addr.sll_family = libc::AF_PACKET as u16;
|
addr.sll_family = libc::AF_PACKET as u16;
|
||||||
@@ -404,7 +404,7 @@ impl LinuxBpfTun {
|
|||||||
|
|
||||||
let bind_ret = unsafe {
|
let bind_ret = unsafe {
|
||||||
libc::bind(
|
libc::bind(
|
||||||
fd.as_raw_fd(),
|
fd.as_ref().as_raw_fd(),
|
||||||
&addr as *const _ as *const libc::sockaddr,
|
&addr as *const _ as *const libc::sockaddr,
|
||||||
mem::size_of::<libc::sockaddr_ll>() as u32,
|
mem::size_of::<libc::sockaddr_ll>() as u32,
|
||||||
)
|
)
|
||||||
@@ -413,7 +413,7 @@ impl LinuxBpfTun {
|
|||||||
return Err(io::Error::last_os_error());
|
return Err(io::Error::last_os_error());
|
||||||
}
|
}
|
||||||
|
|
||||||
let actual_rcvbuf = set_socket_rcvbuf(fd.as_raw_fd(), DEFAULT_RCVBUF_BYTES)?;
|
let actual_rcvbuf = set_socket_rcvbuf(fd.as_ref().as_raw_fd(), DEFAULT_RCVBUF_BYTES)?;
|
||||||
|
|
||||||
let filter = build_tcp_filter(src_addr, dst_addr)?;
|
let filter = build_tcp_filter(src_addr, dst_addr)?;
|
||||||
let mut prog = libc::sock_fprog {
|
let mut prog = libc::sock_fprog {
|
||||||
@@ -425,7 +425,7 @@ impl LinuxBpfTun {
|
|||||||
};
|
};
|
||||||
let opt_ret = unsafe {
|
let opt_ret = unsafe {
|
||||||
libc::setsockopt(
|
libc::setsockopt(
|
||||||
fd.as_raw_fd(),
|
fd.as_ref().as_raw_fd(),
|
||||||
libc::SOL_SOCKET,
|
libc::SOL_SOCKET,
|
||||||
libc::SO_ATTACH_FILTER,
|
libc::SO_ATTACH_FILTER,
|
||||||
&mut prog as *mut _ as *mut libc::c_void,
|
&mut prog as *mut _ as *mut libc::c_void,
|
||||||
@@ -442,7 +442,7 @@ impl LinuxBpfTun {
|
|||||||
};
|
};
|
||||||
let _ = unsafe {
|
let _ = unsafe {
|
||||||
libc::setsockopt(
|
libc::setsockopt(
|
||||||
fd.as_raw_fd(),
|
fd.as_ref().as_raw_fd(),
|
||||||
libc::SOL_SOCKET,
|
libc::SOL_SOCKET,
|
||||||
libc::SO_RCVTIMEO,
|
libc::SO_RCVTIMEO,
|
||||||
&timeout as *const _ as *const libc::c_void,
|
&timeout as *const _ as *const libc::c_void,
|
||||||
@@ -453,10 +453,13 @@ impl LinuxBpfTun {
|
|||||||
let stop = Arc::new(AtomicBool::new(false));
|
let stop = Arc::new(AtomicBool::new(false));
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(1024);
|
let (tx, rx) = tokio::sync::mpsc::channel(1024);
|
||||||
let stop_clone = stop.clone();
|
let stop_clone = stop.clone();
|
||||||
let read_fd = fd.as_raw_fd();
|
let read_fd = fd.as_ref().as_raw_fd();
|
||||||
|
let fd_guard = fd.clone();
|
||||||
let interface_name_for_worker = interface_name.to_string();
|
let interface_name_for_worker = interface_name.to_string();
|
||||||
|
|
||||||
let worker = std::thread::spawn(move || {
|
let worker = std::thread::spawn(move || {
|
||||||
|
// Keep the packet socket alive until the detached worker actually exits.
|
||||||
|
let _fd_guard = fd_guard;
|
||||||
let mut buf = vec![0u8; 65536];
|
let mut buf = vec![0u8; 65536];
|
||||||
let mut stats_enabled = true;
|
let mut stats_enabled = true;
|
||||||
let mut total_packets: u64 = 0;
|
let mut total_packets: u64 = 0;
|
||||||
@@ -562,9 +565,11 @@ impl LinuxBpfTun {
|
|||||||
impl Drop for LinuxBpfTun {
|
impl Drop for LinuxBpfTun {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.stop.store(true, AtomicOrdering::Relaxed);
|
self.stop.store(true, AtomicOrdering::Relaxed);
|
||||||
let _ = unsafe { libc::shutdown(self.fd.as_raw_fd(), libc::SHUT_RD) };
|
let _ = unsafe { libc::shutdown(self.fd.as_ref().as_raw_fd(), libc::SHUT_RD) };
|
||||||
if let Some(worker) = self.worker.take() {
|
if let Some(worker) = self.worker.take() {
|
||||||
let _ = worker.join();
|
// Dropping the JoinHandle detaches the worker. The worker holds its own Arc<OwnedFd>
|
||||||
|
// clone, so the packet socket stays valid until recv wakes up and the thread exits.
|
||||||
|
drop(worker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -602,7 +607,7 @@ impl stack::Tun for LinuxBpfTun {
|
|||||||
|
|
||||||
let ret = unsafe {
|
let ret = unsafe {
|
||||||
libc::sendto(
|
libc::sendto(
|
||||||
self.fd.as_raw_fd(),
|
self.fd.as_ref().as_raw_fd(),
|
||||||
packet.as_ptr() as *const libc::c_void,
|
packet.as_ptr() as *const libc::c_void,
|
||||||
packet.len(),
|
packet.len(),
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
instance_manager::NetworkInstanceManager, proto::rpc_impl::service_registry::ServiceRegistry,
|
instance_manager::NetworkInstanceManager,
|
||||||
rpc_service::api::register_api_rpc_service, web_client::WebClientHooks,
|
proto::{rpc_impl::service_registry::ServiceRegistry, web::DeviceOsInfo},
|
||||||
|
rpc_service::api::register_api_rpc_service,
|
||||||
|
web_client::WebClientHooks,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Controller {
|
pub struct Controller {
|
||||||
token: String,
|
token: String,
|
||||||
hostname: String,
|
hostname: String,
|
||||||
|
device_os: DeviceOsInfo,
|
||||||
manager: Arc<NetworkInstanceManager>,
|
manager: Arc<NetworkInstanceManager>,
|
||||||
hooks: Arc<dyn WebClientHooks>,
|
hooks: Arc<dyn WebClientHooks>,
|
||||||
}
|
}
|
||||||
@@ -16,12 +19,14 @@ impl Controller {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
token: String,
|
token: String,
|
||||||
hostname: String,
|
hostname: String,
|
||||||
|
device_os: DeviceOsInfo,
|
||||||
manager: Arc<NetworkInstanceManager>,
|
manager: Arc<NetworkInstanceManager>,
|
||||||
hooks: Arc<dyn WebClientHooks>,
|
hooks: Arc<dyn WebClientHooks>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Controller {
|
Controller {
|
||||||
token,
|
token,
|
||||||
hostname,
|
hostname,
|
||||||
|
device_os,
|
||||||
manager,
|
manager,
|
||||||
hooks,
|
hooks,
|
||||||
}
|
}
|
||||||
@@ -39,6 +44,10 @@ impl Controller {
|
|||||||
self.hostname.clone()
|
self.hostname.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn device_os(&self) -> DeviceOsInfo {
|
||||||
|
self.device_os.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn register_api_rpc_service(&self, registry: &ServiceRegistry) {
|
pub fn register_api_rpc_service(&self, registry: &ServiceRegistry) {
|
||||||
register_api_rpc_service(&self.manager, registry, Some(self.hooks.clone()));
|
register_api_rpc_service(&self.manager, registry, Some(self.hooks.clone()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{
|
common::{
|
||||||
config::TomlConfigLoader, global_ctx::GlobalCtx, log, scoped_task::ScopedTask,
|
config::TomlConfigLoader, global_ctx::GlobalCtx, log, os_info::collect_device_os_info,
|
||||||
set_default_machine_id, stun::MockStunInfoCollector,
|
scoped_task::ScopedTask, set_default_machine_id, stun::MockStunInfoCollector,
|
||||||
},
|
},
|
||||||
connector::create_connector_by_url,
|
connector::create_connector_by_url,
|
||||||
instance_manager::{DaemonGuard, NetworkInstanceManager},
|
instance_manager::{DaemonGuard, NetworkInstanceManager},
|
||||||
@@ -62,6 +62,7 @@ impl WebClient {
|
|||||||
let controller = Arc::new(controller::Controller::new(
|
let controller = Arc::new(controller::Controller::new(
|
||||||
token.to_string(),
|
token.to_string(),
|
||||||
hostname.to_string(),
|
hostname.to_string(),
|
||||||
|
collect_device_os_info(),
|
||||||
manager,
|
manager,
|
||||||
hooks,
|
hooks,
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ impl Session {
|
|||||||
let inst_id = uuid::Uuid::new_v4();
|
let inst_id = uuid::Uuid::new_v4();
|
||||||
let token = controller.upgrade().unwrap().token();
|
let token = controller.upgrade().unwrap().token();
|
||||||
let hostname = controller.upgrade().unwrap().hostname();
|
let hostname = controller.upgrade().unwrap().hostname();
|
||||||
|
let device_os = controller.upgrade().unwrap().device_os();
|
||||||
|
|
||||||
let ctx_clone = ctx.clone();
|
let ctx_clone = ctx.clone();
|
||||||
let mut tick = interval(std::time::Duration::from_secs(1));
|
let mut tick = interval(std::time::Duration::from_secs(1));
|
||||||
@@ -91,6 +92,7 @@ impl Session {
|
|||||||
easytier_version: EASYTIER_VERSION.to_string(),
|
easytier_version: EASYTIER_VERSION.to_string(),
|
||||||
hostname: hostname.clone(),
|
hostname: hostname.clone(),
|
||||||
report_time: chrono::Local::now().to_rfc3339(),
|
report_time: chrono::Local::now().to_rfc3339(),
|
||||||
|
device_os: Some(device_os.clone()),
|
||||||
|
|
||||||
running_network_instances: controller
|
running_network_instances: controller
|
||||||
.list_network_instance_ids()
|
.list_network_instance_ids()
|
||||||
|
|||||||
Reference in New Issue
Block a user