use crate::defer; use anyhow::{Context, anyhow, bail}; use axum::body::Body; use axum::http::{HeaderMap, HeaderValue, StatusCode, Uri, header}; use axum::response::IntoResponse; use axum::{ Json, Router, extract::{Query, Request, State}, middleware, response::Response, routing::{get, post}, }; use ipnet::Ipv4Net; use mime_guess::from_path; use parking_lot::Mutex; use rust_embed::RustEmbed; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::{Instant, SystemTime, UNIX_EPOCH}; use time::{OffsetDateTime, format_description}; use tokio::fs; use tokio::net::TcpListener; use tower_http::cors::{Any, CorsLayer}; use vnt_core::api::VntApi; use vnt_core::context::config::Config as CoreConfig; use vnt_core::core::{DEFAULT_MTU, NetworkManager, RegisterResponse}; use vnt_core::nat::NetInput; use vnt_core::port_mapping::PortMapping; use vnt_core::tls::verifier::CertValidationMode; use vnt_core::tunnel_core::server::transport::config::ProtocolAddress; use vnt_core::utils::task_control::TaskGroupManager; const CONFIG_DIR: &str = "vnt_config"; const CURRENT_CONFIG_RECORD: &str = "vnt_current_config.txt"; #[derive(Serialize, Clone, Copy, PartialEq, Eq, Default)] #[serde(rename_all = "lowercase")] enum VntStatus { #[default] Stopped, Starting, Running, } #[derive(Clone)] struct HttpAppState { task_group_manager: TaskGroupManager, inner: Arc>, } #[derive(Default)] struct HttpAppStateInner { vnt: Option, status: VntStatus, start_logs: Vec, } impl HttpAppState { fn starting(&self) -> anyhow::Result<()> { let mut inner = self.inner.lock(); if inner.status != VntStatus::Stopped { return Err(anyhow!("VNT is already starting or running")); } if inner.vnt.is_some() { return Err(anyhow!("VNT is already running")); } inner.status = VntStatus::Starting; inner.start_logs.clear(); Ok(()) } fn stopped(&self) { let mut inner = self.inner.lock(); inner.vnt.take(); inner.status = VntStatus::Stopped; } fn starting_to_stopped(&self) { let mut inner = self.inner.lock(); if inner.status != VntStatus::Starting { return; } inner.vnt.take(); inner.status = VntStatus::Stopped; inner .start_logs .push(format!("[{}] 启动中断", HttpAppState::timestamp())); } fn starting_to_running(&self) { let mut inner = self.inner.lock(); if inner.status != VntStatus::Starting { log::error!("starting_to_running VNT is not starting"); return; } inner.status = VntStatus::Running; inner.start_logs.clear(); } fn record_log(&self, msg: impl Into) { let mut inner = self.inner.lock(); if inner.status != VntStatus::Starting { return; } inner .start_logs .push(format!("[{}] {}", Self::timestamp(), msg.into())); } fn record_log_and_stopped(&self, msg: impl Into) { let mut inner = self.inner.lock(); if inner.status != VntStatus::Starting { return; } inner .start_logs .push(format!("[{}] {}", Self::timestamp(), msg.into())); inner.status = VntStatus::Stopped; } fn status(&self) -> VntStatus { self.inner.lock().status } fn timestamp() -> String { let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); now.format(&format) .unwrap_or_else(|_| "00:00:00".to_string()) } } struct VntHandler { api: VntApi, config_name: String, config_file_name: String, } #[derive(Serialize)] struct ApiResponse { code: i32, msg: String, data: Option, } impl ApiResponse { fn success(data: T) -> Self { Self { code: 0, msg: "success".to_string(), data: Some(data), } } fn error(msg: impl Into) -> Self { Self { code: -1, msg: msg.into(), data: None, } } } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct StartConfig { pub config_name: Option, pub server: Vec, pub cert_mode: Option, pub network_code: String, pub device_id: Option, pub device_name: Option, pub tun_name: Option, pub ip: Option, pub password: Option, #[serde(default)] pub no_punch: bool, #[serde(default)] pub compress: bool, #[serde(default)] pub rtx: bool, #[serde(default)] pub fec: bool, #[serde(default)] pub input: Vec, #[serde(default)] pub output: Vec, #[serde(default)] pub no_nat: bool, #[serde(default)] pub no_tun: bool, pub mtu: Option, #[serde(default)] pub port_mapping: Vec, #[serde(default)] pub allow_mapping: bool, #[serde(default)] pub udp_stun: Vec, #[serde(default)] pub tcp_stun: Vec, pub tunnel_port: Option, } #[derive(Deserialize)] struct SaveConfigReq { file_name: Option, config: String, } #[derive(Deserialize)] struct FileReq { file_name: String, } #[derive(Serialize)] struct ConfigSummary { file_name: String, config_name: String, } #[derive(Serialize, Default)] struct HttpAppInfo { name: String, version: String, ip: Option, prefix_len: Option, gateway: Option, device_id: String, status: VntStatus, current_config_name: Option, current_config_file: Option, online_client_num: usize, offline_client_num: usize, direct_client_num: usize, server_info: Vec, nat_type: Option, public_ipv6: Option, public_ipv4s: Vec, network_code: Option, mtu: Option, fec: Option, compress: Option, encrypt: Option, rtx: Option, } #[derive(Serialize)] struct HttpServerInfo { server: String, connected: bool, server_rtt: Option, server_version: Option, } #[derive(Serialize)] struct HttpClientItem { ip: Ipv4Addr, name: Option, online: bool, route: Option, version: String, last_connected_time: i64, key_equal: i32, nat_info: Option, packet_loss: Option, traffic: Option, } #[derive(Serialize)] struct HttpClientNatInfo { nat_type: String, public_ips: Vec, ipv6: Option, } #[derive(Serialize)] struct HttpPacketLoss { sent: u64, received: u64, loss_rate: f64, } #[derive(Serialize)] struct HttpTraffic { tx_bytes: u64, rx_bytes: u64, } #[derive(Serialize)] struct HttpRouteItem { ip: Ipv4Addr, routes: Vec, } #[derive(Serialize)] struct HttpRouteDetail { addr: String, protocol: String, metric: u8, rtt: u32, loss_rate: u16, } #[derive(Serialize)] struct StartStatusResponse { status: VntStatus, logs: Vec, } async fn get_start_status( State(state): State, ) -> Json> { let lock = state.inner.lock(); Json(ApiResponse::success(StartStatusResponse { status: lock.status, logs: lock.start_logs.clone(), })) } async fn logging_middleware(req: Request, next: axum::middleware::Next) -> Response { let method = req.method().clone(); let uri = req.uri().clone(); let start = Instant::now(); let response = next.run(req).await; log::info!( "Request: {} {} | Status: {} | Took: {:?}", method, uri, response.status(), start.elapsed() ); response } #[derive(RustEmbed)] #[folder = "static/"] struct Asset; pub async fn run_http_server( addr: SocketAddr, start_config_file_name: Option, ) -> anyhow::Result<()> { fs::create_dir_all(CONFIG_DIR) .await .context("Failed to create config directory")?; let state = HttpAppState { task_group_manager: TaskGroupManager::new(), inner: Arc::new(Default::default()), }; // 自动启动逻辑 let auto_start_file = determine_auto_start_file(start_config_file_name).await; if let Some((file_name, path)) = auto_start_file { log::info!("Auto starting VNT with config: {:?}", path); let state_clone = state.clone(); tokio::spawn(async move { if let Err(e) = start_vnt_internal(&state_clone, file_name, path).await { log::error!("Auto start failed: {:?}", e); } }); } let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any); let app = Router::new() .route("/api/info", get(get_info)) .route("/api/peers", get(get_peers)) .route("/api/routes", get(get_routes)) .route("/api/start/status", get(get_start_status)) .route("/api/start", post(start_vnt_handler)) .route("/api/stop", post(stop_vnt_handler)) .route("/api/restart", post(restart_vnt_handler)) .route("/api/config/list", get(list_configs)) .route( "/api/config", get(get_config).post(save_config).delete(delete_config), ) .layer(cors) .layer(middleware::from_fn(logging_middleware)) .with_state(state) .fallback(static_handler); log::info!("HTTP API Listening on http://{}", addr); let listener = TcpListener::bind(addr).await?; axum::serve(listener, app) .with_graceful_shutdown(shutdown_signal()) .await?; Ok(()) } /// 确定自动启动的配置文件 async fn determine_auto_start_file( start_config_file_name: Option, ) -> Option<(String, PathBuf)> { let path = if let Some(name) = start_config_file_name { Some(name) } else if Path::new(CURRENT_CONFIG_RECORD).exists() { fs::read_to_string(CURRENT_CONFIG_RECORD) .await .ok() .filter(|content| !content.trim().is_empty()) .map(|content| Path::new(CONFIG_DIR).join(content.trim())) } else { None }; path.and_then(|p| { let file_name = p.file_name()?.to_str()?.to_string(); if p.exists() { Some((file_name, p)) } else { log::warn!("Auto start config file not found: {:?}", p); None } }) } fn build_headers_for_path(path: &str) -> HeaderMap { let mut headers = HeaderMap::new(); let is_gz = path.ends_with(".gz"); let mime = if is_gz { let original = path.trim_end_matches(".gz"); from_path(original).first_or_octet_stream() } else { from_path(path).first_or_octet_stream() }; headers.insert( header::CONTENT_TYPE, HeaderValue::from_str(mime.as_ref()).unwrap(), ); if is_gz { headers.insert(header::CONTENT_ENCODING, HeaderValue::from_static("gzip")); headers.insert(header::VARY, HeaderValue::from_static("Accept-Encoding")); } headers.insert( header::CACHE_CONTROL, HeaderValue::from_static("public, max-age=31536000, immutable"), ); headers } async fn static_handler(uri: Uri) -> impl IntoResponse { let path = uri.path().trim_start_matches('/'); let path = if path.is_empty() { "index.html" } else { path }; // 先尝试从本地文件读取 let local_path = Path::new("static").join(path); if local_path.is_file() && let Ok(content) = tokio::fs::read(&local_path).await { log::debug!("Serving file from local filesystem: {:?}", local_path); let mime = from_path(&local_path).first_or_octet_stream(); return ([(header::CONTENT_TYPE, mime.as_ref())], content).into_response(); } // 从内嵌数据中读取 if let Some(content) = Asset::get(path) { log::debug!("Serving file from embedded assets: {}", path); let headers = build_headers_for_path(path); return (headers, Body::from(content.data)).into_response(); } (StatusCode::NOT_FOUND, "404 Not Found").into_response() } /// 启动 VNT 服务的入口函数 async fn start_vnt_internal( state: &HttpAppState, file_name: String, file_path: PathBuf, ) -> anyhow::Result<()> { log::info!("Starting VNT service: {}", file_name); state.starting()?; let state_for_error = state.clone(); let on_error_guard = defer(move || { state_for_error.starting_to_stopped(); }); state.record_log(format!("启动配置: {}", file_name)); state.record_log("读取配置文件"); // 读取并解析配置 let content = fs::read_to_string(&file_path) .await .with_context(|| format!("Config file not found: {:?}", file_path))?; state.record_log("解析配置文件内容"); let cfg: StartConfig = toml::from_str(&content).context("Failed to parse TOML config")?; let config_display_name = cfg.config_name.clone().unwrap_or_else(|| file_name.clone()); let core_config = convert_config(cfg)?; let sub_input = core_config.input.clone(); state.record_log("创建异步任务组"); let (task_group, task_group_guard) = state .task_group_manager .create_task() .context("Create task failed")?; state.record_log("创建组网管理器"); let state_clone = state.clone(); tokio::spawn(async move { let result = start_vnt_network( state_clone.clone(), file_name, config_display_name, core_config, sub_input, task_group, task_group_guard, ) .await; if let Err(e) = result { log::error!("Failed to start VNT network: {:?}", e); state_clone.record_log_and_stopped(format!("启动失败: {}", e)); } drop(on_error_guard); }); Ok(()) } /// 执行实际的网络启动操作 async fn start_vnt_network( state: HttpAppState, file_name: String, config_display_name: String, core_config: CoreConfig, sub_input: Vec, task_group: vnt_core::utils::task_control::TaskGroup, task_group_guard: vnt_core::utils::task_control::TaskGroupGuard, ) -> anyhow::Result<()> { let mut network_manager = NetworkManager::create_network(Box::new(core_config), task_group.clone()) .await .map_err(|e| anyhow!("Create network failed: {:?}", e))?; let vnt_api = network_manager.vnt_api(); { let mut lock = state.inner.lock(); if lock.vnt.is_some() { return Err(anyhow!("VNT is already running")); } lock.vnt = Some(VntHandler { api: vnt_api, config_name: config_display_name, config_file_name: file_name.clone(), }); } let state_for_vnt_cleanup = state.clone(); let vnt_cleanup_guard = defer(move || { state_for_vnt_cleanup.stopped(); }); state.record_log("连接服务器,执行注册"); log::info!("Registering with server"); let reg_msg = loop { let reg_msg = match network_manager.register().await { Ok(rs) => rs, Err(e) => { log::error!("Register failed: {:?}", e); state.record_log(format!("注册失败:{},5秒后重试", e)); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; continue; } }; match reg_msg { RegisterResponse::Success(reg_msg) => { break reg_msg; } RegisterResponse::Failed(e) => { log::error!("Register failed: {:?}", e); bail!("注册失败:{}", e.message) } } }; state.record_log(format!("注册成功 {}/{}", reg_msg.ip, reg_msg.prefix_len)); log::info!("Network Started: {}/{}", reg_msg.ip, reg_msg.prefix_len); if !network_manager.is_no_tun() { state.record_log("正在创建 TUN 虚拟网卡"); network_manager.start_tun().await?; state.record_log("创建 TUN 虚拟网卡成功,设置 IP"); network_manager .set_tun_network_ip(reg_msg.ip, reg_msg.prefix_len) .await?; state.record_log("设置 IP 成功"); // 配置子网路由 if !sub_input.is_empty() && let Ok(if_index) = network_manager.tun_if_index().await && let Ok(mut route_manager) = route_manager::RouteManager::new() { state.record_log("配置子网路由"); for input in &sub_input { let route = route_manager::Route::new(input.net.network().into(), input.net.prefix_len()) .with_gateway(input.target_ip.into()) .with_if_index(if_index); if let Err(e) = route_manager.add(&route) { log::error!("add route [{route}] error: {e:?}"); } else { log::info!("add route [{route}] successful"); } } } } state.starting_to_running(); // 启动网络管理任务 task_group.spawn(async move { network_manager.wait_all_stopped().await; drop(task_group_guard); drop(network_manager); drop(vnt_cleanup_guard); log::info!("Network manager stopped."); }); // 记录当前配置 if let Err(e) = fs::write(CURRENT_CONFIG_RECORD, &file_name).await { log::warn!("Failed to record current config: {}", e); } Ok(()) } fn is_valid_file_name(file_name: &str) -> bool { !file_name.is_empty() && !file_name.contains("..") && !file_name.contains('/') && !file_name.contains('\\') } async fn start_vnt_handler( State(state): State, Json(req): Json, ) -> Json> { if !is_valid_file_name(&req.file_name) { return Json(ApiResponse::error("Invalid file name")); } let path = Path::new(CONFIG_DIR).join(&req.file_name); if !path.exists() { return Json(ApiResponse::error("Config file not found")); } match start_vnt_internal(&state, req.file_name, path).await { Ok(_) => Json(ApiResponse::success(())), Err(e) => Json(ApiResponse::error(format!("Start failed: {:?}", e))), } } async fn stop_vnt_handler(State(state): State) -> Json> { if state.status() == VntStatus::Stopped { return Json(ApiResponse::error("Vnt stopped")); } state.task_group_manager.stop(); let _ = fs::write(CURRENT_CONFIG_RECORD, "").await; Json(ApiResponse::success(())) } async fn restart_vnt_handler( State(state): State, Json(req): Json, ) -> Json> { if !is_valid_file_name(&req.file_name) { return Json(ApiResponse::error("Invalid file name")); } let path = Path::new(CONFIG_DIR).join(&req.file_name); if !path.exists() { return Json(ApiResponse::error("Config file not found")); } // 先停止(如果正在运行则停止,否则忽略) if state.status() != VntStatus::Stopped { state.task_group_manager.stop(); // 等待停止完成 for _ in 0..50 { if state.status() == VntStatus::Stopped { break; } tokio::time::sleep(std::time::Duration::from_millis(100)).await; } } // 再启动 match start_vnt_internal(&state, req.file_name, path).await { Ok(_) => Json(ApiResponse::success(())), Err(e) => Json(ApiResponse::error(format!("Restart failed: {:?}", e))), } } async fn get_info(State(state): State) -> Json> { let lock = state.inner.lock(); let status = lock.status; let info = if let Some(handler) = lock.vnt.as_ref() { let api = &handler.api; let config = api.get_config(); let ips = api.client_ips(); let server_node_list = api.server_node_list(); let nat_info = api.nat_info(); let network = api.network(); HttpAppInfo { name: config .as_ref() .map(|v| v.device_name.clone()) .unwrap_or_default(), version: env!("CARGO_PKG_VERSION").to_string(), ip: network.map(|v| v.ip), prefix_len: network.map(|v| v.prefix_len), gateway: network.map(|v| v.gateway), device_id: config .as_ref() .map(|v| v.device_id.clone()) .unwrap_or_default(), status, current_config_name: Some(handler.config_name.clone()), current_config_file: Some(handler.config_file_name.clone()), online_client_num: ips.iter().filter(|v| v.online).count(), offline_client_num: ips.iter().filter(|v| !v.online).count(), direct_client_num: ips.iter().filter(|ip| api.is_direct(&ip.ip)).count(), server_info: server_node_list .into_iter() .map(|v| HttpServerInfo { server: v.server_addr.to_string(), connected: v.connected, server_rtt: v.rtt, server_version: v.server_version, }) .collect(), nat_type: nat_info.as_ref().map(|v| format!("{:?}", v.nat_type)), public_ipv4s: nat_info .as_ref() .map(|v| v.public_ips.clone()) .unwrap_or_default(), public_ipv6: nat_info.as_ref().and_then(|v| v.ipv6), network_code: config.as_ref().map(|v| v.network_code.clone()), mtu: config.as_ref().map(|v| v.mtu.unwrap_or(DEFAULT_MTU)), fec: config.as_ref().map(|v| v.fec), compress: config.as_ref().map(|v| v.compress), encrypt: config.as_ref().map(|v| v.password.is_some()), rtx: config.as_ref().map(|v| v.rtx), } } else { HttpAppInfo { version: env!("CARGO_PKG_VERSION").to_string(), status, ..Default::default() } }; Json(ApiResponse::success(info)) } async fn list_configs() -> Json>> { let mut result = Vec::new(); let Ok(mut entries) = fs::read_dir(CONFIG_DIR).await else { return Json(ApiResponse::success(result)); }; while let Ok(Some(entry)) = entries.next_entry().await { let path = entry.path(); if path.extension().is_none_or(|ext| ext != "toml") { continue; } let Ok(content) = fs::read_to_string(&path).await else { continue; }; match toml::from_str::(&content) { Ok(cfg) => { let file_name = path .file_name() .and_then(|s| s.to_str()) .unwrap_or("") .to_string(); result.push(ConfigSummary { file_name, config_name: cfg .config_name .unwrap_or_else(|| entry.file_name().to_string_lossy().to_string()), }); } Err(e) => { log::warn!("Failed to parse configuration file {:?}: {:?}", path, e); } } } result.sort_by(|a, b| b.file_name.cmp(&a.file_name)); Json(ApiResponse::success(result)) } async fn save_config(Json(req): Json) -> Json> { // 验证配置格式 if let Err(e) = toml::from_str::(&req.config) { log::warn!("Failed to parse configuration: {:?}", e); return Json(ApiResponse::error(format!("Invalid TOML format: {}", e))); } let file_name = req .file_name .filter(|name| !name.is_empty()) .unwrap_or_else(|| { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis(); format!("{}.toml", now) }); if !is_valid_file_name(&file_name) { return Json(ApiResponse::error("Invalid file name")); } let target_path = Path::new(CONFIG_DIR).join(&file_name); match fs::write(&target_path, &req.config).await { Ok(_) => Json(ApiResponse::success(())), Err(e) => Json(ApiResponse::error(format!("Write config failed: {}", e))), } } async fn get_config(Query(req): Query) -> Json> { if !is_valid_file_name(&req.file_name) { return Json(ApiResponse::error("Invalid file name")); } let path = Path::new(CONFIG_DIR).join(&req.file_name); if !path.exists() { return Json(ApiResponse::error("Config file not found")); } match fs::read_to_string(&path).await { Ok(content) => Json(ApiResponse::success(content)), Err(e) => Json(ApiResponse::error(format!("Read file failed: {}", e))), } } async fn delete_config( State(state): State, Query(req): Query, ) -> Json> { if !is_valid_file_name(&req.file_name) { return Json(ApiResponse::error("Invalid file name")); } { if let Some(vnt) = &state.inner.lock().vnt && vnt.config_file_name == req.file_name { return Json(ApiResponse::error("此配置已被使用,不能删除")); } } let path = Path::new(CONFIG_DIR).join(&req.file_name); if !path.exists() { return Json(ApiResponse::error("Config file not found")); } match fs::remove_file(&path).await { Ok(_) => Json(ApiResponse::success(())), Err(e) => Json(ApiResponse::error(format!("Delete failed: {}", e))), } } fn convert_config(cfg: StartConfig) -> anyhow::Result { let server_addrs: Vec = cfg .server .iter() .map(|s| { s.parse() .map_err(|e| anyhow!("invalid server address '{}': {}", s, e)) }) .collect::>()?; let port_mapping: Vec = cfg .port_mapping .iter() .map(|s| { s.parse() .map_err(|e| anyhow!("invalid port_mapping '{}': {}", s, e)) }) .collect::>()?; let cert_mode = match cfg.cert_mode.as_deref() { Some(s) => s .parse() .map_err(|e| anyhow!("invalid cert_mode '{}': {}", s, e))?, None => CertValidationMode::InsecureSkipVerification, }; let device_id = match cfg.device_id { Some(id) => id, None => vnt_core::utils::device_id::get_device_id() .map_err(|e| anyhow!("failed to get device_id: {}", e))?, }; let device_name = cfg.device_name.unwrap_or_else(|| { hostname::get() .ok() .and_then(|v| v.into_string().ok()) .unwrap_or_default() }); let mut udp_stun = cfg.udp_stun; for x in udp_stun.iter_mut() { if !x.contains(':') { x.push_str(":3478"); } } let mut tcp_stun = cfg.tcp_stun; for x in tcp_stun.iter_mut() { if !x.contains(':') { x.push_str(":3478"); } } Ok(CoreConfig { server_addr: server_addrs, network_code: cfg.network_code, ip: cfg.ip, no_punch: cfg.no_punch, rtx: cfg.rtx, compress: cfg.compress, device_id, device_name, tun_name: cfg.tun_name, password: cfg.password, cert_mode, input: cfg.input, output: cfg.output, no_nat: cfg.no_nat, no_tun: cfg.no_tun, mtu: cfg.mtu, port_mapping, allow_port_mapping: cfg.allow_mapping, udp_stun, tcp_stun, fec: cfg.fec, tunnel_port: cfg.tunnel_port, }) } async fn shutdown_signal() { let ctrl_c = async { tokio::signal::ctrl_c() .await .expect("failed to install Ctrl+C handler"); }; #[cfg(unix)] let terminate = async { tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) .expect("failed to install signal handler") .recv() .await; }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => {}, _ = terminate => {}, } } async fn get_peers(State(state): State) -> Json>> { let api = state.inner.lock().vnt.as_ref().map(|v| v.api.clone()); let Some(api) = api else { return Json(ApiResponse::error("VNT not running")); }; let key_sign = api.get_config().and_then(|config| config.key_sign()); let calc_key_equal = |peer_key_sign: &Option| -> i32 { match (&key_sign, peer_key_sign) { (None, None) => 2, (Some(k1), Some(k2)) if k1 == k2 => 1, (Some(_), Some(_)) => 5, (Some(_), None) => 3, (None, Some(_)) => 4, } }; let build_nat_info = |ip: &Ipv4Addr| -> Option { api.peer_nat_info(ip).map(|v| HttpClientNatInfo { nat_type: format!("{:?}", v.nat_type), public_ips: v.public_ips, ipv6: v.ipv6, }) }; let build_packet_loss = |ip: &Ipv4Addr| -> Option { api.packet_loss_info(ip).map(|v| HttpPacketLoss { sent: v.sent, received: v.received, loss_rate: v.loss_rate, }) }; let build_traffic = |ip: &Ipv4Addr| -> Option { api.traffic_info(ip).map(|v| HttpTraffic { tx_bytes: v.tx_bytes, rx_bytes: v.rx_bytes, }) }; let build_route = |ip: &Ipv4Addr| -> Option { api.find_route(ip).map(|route| HttpRouteDetail { addr: route.route_key().to_string(), protocol: route.route_key().protocol().to_string(), metric: route.metric(), rtt: route.rtt(), loss_rate: route.loss_rate(), }) }; // 先从本地获取基础数据 let mut merged: HashMap = api .client_ips() .into_iter() .map(|v| { let ip = v.ip; let route = build_route(&ip); // 如果有路由,说明设备在线(可以直接通信) let has_route = route.is_some(); ( ip, HttpClientItem { ip, name: None, online: v.online || has_route, route, version: String::new(), last_connected_time: 0, key_equal: 0, nat_info: build_nat_info(&ip), packet_loss: build_packet_loss(&ip), traffic: build_traffic(&ip), }, ) }) .collect(); // 从服务器获取更详细的信息 if let Ok(resp) = api.server_rpc().client_list().await { for v in resp.list { let ip = Ipv4Addr::from(v.ip); let route = build_route(&ip); // 如果有路由,说明设备在线(可以直接通信) let has_route = route.is_some(); merged.insert( ip, HttpClientItem { ip, name: Some(v.name), online: v.online || has_route, route, version: v.version, last_connected_time: v.last_connected_time, key_equal: calc_key_equal(&v.key_sign), nat_info: build_nat_info(&ip), packet_loss: build_packet_loss(&ip), traffic: build_traffic(&ip), }, ); } } else { log::warn!("Failed to get client list from server"); } let mut items: Vec = merged.into_values().collect(); items.sort_by_key(|it| it.ip); Json(ApiResponse::success(items)) } async fn get_routes(State(state): State) -> Json>> { let lock = state.inner.lock(); let Some(handler) = lock.vnt.as_ref() else { return Json(ApiResponse::error("VNT not running")); }; let table = handler.api.route_table(); let items: Vec = table .into_iter() .map(|(ip, route_list)| HttpRouteItem { ip, routes: route_list .into_iter() .map(|v| HttpRouteDetail { addr: v.route_key().to_string(), protocol: v.route_key().protocol().to_string(), metric: v.metric(), rtt: v.rtt(), loss_rate: v.loss_rate(), }) .collect(), }) .collect(); Json(ApiResponse::success(items)) }