This commit is contained in:
lbl
2026-02-10 18:20:39 +08:00
parent 71da72da34
commit b4301a8106
261 changed files with 20179 additions and 21547 deletions
+285
View File
@@ -0,0 +1,285 @@
use crate::message::ipc_request::IpcCmd;
use crate::message::ipc_response::ResponsePayload;
use crate::message::{
AppInfo, ClientInfoList, ClientIpList, ClientRouteList, IpcRequest, IpcResponse,
};
use crate::{DEFAULT_PORT, get_port_file_path};
use anyhow::Context;
use cli_table::{Cell, Style, Table, print_stdout};
use console::style;
use futures::{SinkExt, StreamExt};
use prost::Message;
use std::fs;
use std::net::Ipv4Addr;
use std::time::Duration;
use time::macros::format_description;
use time::{OffsetDateTime, UtcOffset};
use tokio::net::TcpStream;
use tokio_util::codec::{Framed, LengthDelimitedCodec};
pub async fn run_client(cmd: IpcCmd, port: Option<u16>) -> anyhow::Result<()> {
let path = get_port_file_path();
let port = if let Some(port) = port {
port
} else {
match fs::read_to_string(&path) {
Ok(port_string) => port_string.trim().parse().context("parse port error")?,
Err(_) => DEFAULT_PORT,
}
};
let addr = format!("127.0.0.1:{}", port);
let stream = tokio::time::timeout(Duration::from_secs(3), TcpStream::connect(&addr))
.await
.context("Connection timed out")??;
let mut framed = Framed::new(stream, LengthDelimitedCodec::new());
framed
.send(IpcRequest { ipc_cmd: Some(cmd) }.encode_to_vec().into())
.await?;
let response = framed.next().await.context("Unexpected end of stream")??;
let response = IpcResponse::decode(response).context("decode response error")?;
match response
.response_payload
.context("response payload is empty")?
{
ResponsePayload::AppInfo(info) => {
print_app_info(info);
}
ResponsePayload::ClientIps(list) => print_client_ip_list(list)?,
ResponsePayload::ClientList(list) => {
print_client_list(list)?;
}
ResponsePayload::AllRoute(route_list) => {
print_route_list(route_list)?;
}
};
Ok(())
}
fn key_style(s: &str) -> console::StyledObject<&str> {
style(s).cyan().bright().bold()
}
pub fn print_app_info(info: AppInfo) {
println!(
"\n{}",
style("--- Application Information ---").italic().dim()
);
for server_info in info.server_info {
let status_colored = if server_info.connected {
if let Some(rtt) = server_info.server_rtt {
style(format!("Online {rtt}ms"))
.green()
.bright()
.to_string()
} else {
style("Online").green().bright().to_string()
}
} else {
style("Offline").red().bright().to_string()
};
println!(
"{:24}{} ({})",
key_style("Server:"),
style(&server_info.server).white(),
status_colored
);
let time_str = server_info
.last_connected_time
.map(|v| style(ts_to_string(v / 1000)).white().to_string())
.unwrap_or_else(|| "Never".to_string());
println!("{:24}{}", key_style("Last Connected Time:"), time_str);
}
println!("{:24}{}", key_style("Name:"), style(&info.name).white());
println!("{:24}{}", key_style("Id:"), style(&info.device_id).white());
println!(
"{:24}{}",
key_style("Version:"),
style(&info.version).white()
);
let ip_str = info
.ip
.map(|v| {
style(Ipv4Addr::from(v).to_string())
.yellow()
.bright()
.to_string()
})
.unwrap_or_else(|| style("N/A").red().bright().to_string());
println!("{:24}{}", key_style("IP:"), ip_str);
let total_clients = info.online_client_num + info.offline_client_num;
println!(
"{:24}{}",
key_style("Total Clients:"),
style(total_clients).color256(213)
);
println!(
"{:24}{}",
key_style("Online Clients:"),
style(info.online_client_num).color256(82)
);
println!(
"{:24}{}",
key_style("P2P Clients:"),
style(info.direct_client_num).color256(117)
);
println!(
"{:24}{}",
key_style("Nat Type:"),
style(info.nat_type.unwrap_or_else(|| "Unknown".to_string())).color256(117)
);
println!(
"{:24}{}",
key_style("Ipv6:"),
style(info.public_ipv6.unwrap_or_default()).color256(117)
);
println!(
"{:24}{}",
key_style("Public Ipv4:"),
style(info.public_ipv4s.join(",")).color256(117)
);
println!(
"{}",
style("-------------------------------").italic().dim()
);
}
fn print_client_ip_list(list: ClientIpList) -> anyhow::Result<()> {
println!("\n--- Client List ({}) ---", list.items.len());
let table = list
.items
.iter()
.map(|item| {
vec![
Ipv4Addr::from(item.ip).to_string().cell(),
item.online.to_string().cell(),
item.is_direct.to_string().cell(),
item.rtt.map(|v| v.to_string()).unwrap_or_default().cell(),
]
})
.table()
.title(vec![
"IP".cell().bold(true),
"Online".cell().bold(true),
"P2P".cell().bold(true),
"RTT".cell().bold(true),
]);
print_stdout(table)?;
println!("\n");
Ok(())
}
fn print_client_list(list: ClientInfoList) -> anyhow::Result<()> {
println!("\n--- Client List ({}) ---", list.items.len());
let table = list
.items
.iter()
.map(|item| {
let key_equal: bool = item.key_equal;
let mut ip_str = Ipv4Addr::from(item.ip).to_string();
if !key_equal {
ip_str.push_str("(Key Mismatch)");
}
let loss_str = item
.packet_loss
.as_ref()
.map(|v| format!("{:.1}%", v.loss_rate))
.unwrap_or_default();
vec![
ip_str.cell(),
item.name.clone().cell(),
item.version.clone().cell(),
item.online.to_string().cell(),
item.is_direct.to_string().cell(),
item.rtt.map(|v| v.to_string()).unwrap_or_default().cell(),
loss_str.cell(),
ts_to_string(item.last_connected_time).cell(),
]
})
.table()
.title(vec![
"IP".cell().bold(true),
"Name".cell().bold(true),
"Version".cell().bold(true),
"Online".cell().bold(true),
"P2P".cell().bold(true),
"RTT".cell().bold(true),
"Loss".cell().bold(true),
"Last Connected Time".cell().bold(true),
]);
print_stdout(table)?;
println!("\n");
Ok(())
}
pub fn print_route_list(route_list: ClientRouteList) -> anyhow::Result<()> {
let total_routes: usize = route_list
.items
.iter()
.map(|item| item.route_list.len())
.sum();
println!("\n--- All Routes List (Total: {}) ---", total_routes);
let mut rows = Vec::new();
for client_route in route_list.items {
let client_ip_str = Ipv4Addr::from(client_route.ip).to_string();
for route in client_route.route_list {
rows.push(vec![
client_ip_str.clone().cell(),
route.metric.to_string().cell(),
route.rtt.to_string().cell(),
route.addr.clone().cell(),
]);
}
}
let table = rows.table().title(vec![
"Destination IP".cell().bold(true),
"Metric".cell().bold(true),
"RTT (ms)".cell().bold(true),
"Remote Address".cell().bold(true),
]);
print_stdout(table)?;
println!("\n");
Ok(())
}
pub fn ts_to_string(ts_secs: i64) -> String {
let dt = match OffsetDateTime::from_unix_timestamp(ts_secs) {
Ok(dt) => dt,
Err(_) => {
return String::new();
}
};
let local_offset = match UtcOffset::local_offset_at(dt) {
Ok(offset) => offset,
Err(_e) => match UtcOffset::from_hms(8, 0, 0) {
Ok(offset) => offset,
Err(_) => return String::new(),
},
};
let dt_local = dt.to_offset(local_offset);
let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
dt_local.format(&format).unwrap()
}
+12
View File
@@ -0,0 +1,12 @@
pub mod client;
pub mod message;
pub mod server;
pub use vnt_core::*;
const DEFAULT_PORT: u16 = 11233;
const PORT_FILE: &str = "PORT";
fn get_port_file_path() -> std::path::PathBuf {
std::path::PathBuf::from(PORT_FILE)
}
+4
View File
@@ -0,0 +1,4 @@
mod proto {
include!(concat!(env!("OUT_DIR"), "/protocol.local_ipc.rs"));
}
pub use proto::*;
+194
View File
@@ -0,0 +1,194 @@
use crate::{DEFAULT_PORT, get_port_file_path};
use crate::message::ipc_request::IpcCmd;
use crate::message::ipc_response::ResponsePayload;
use crate::message::{
AppInfo, ClientInfoItem, ClientInfoList, ClientIpItem, ClientIpList, ClientRouteItem,
ClientRouteList, IpcRequest, IpcResponse, PacketLoss, Route, ServerInfo,
};
use anyhow::bail;
use futures::{SinkExt, StreamExt};
use prost::Message;
use std::fs;
use std::net::Ipv4Addr;
use tokio::io::{self};
use tokio::net::{TcpListener, TcpStream};
use tokio_util::codec::{Framed, LengthDelimitedCodec};
use vnt_core::api::VntApi;
async fn handle_connection(stream: TcpStream, vnt_api: VntApi) -> anyhow::Result<()> {
let mut framed = Framed::new(stream, LengthDelimitedCodec::new());
if let Some(Ok(message)) = framed.next().await {
let request = IpcRequest::decode(message.as_ref())?;
let Some(cmd) = request.ipc_cmd else {
bail!("Received an IpcRequest but it was None");
};
let rs = match cmd {
IpcCmd::AppInfo(_) => {
let info = app_info(&vnt_api);
ResponsePayload::AppInfo(info)
}
IpcCmd::ClientIps(_) => {
let items = vnt_api
.client_ips()
.into_iter()
.map(|v| {
let rtt = if let Some(v) = vnt_api.find_route(&v.ip) {
Some(v.rtt())
} else {
vnt_api.server_node_rtt(&v.ip).map(|v| v * 2)
};
ClientIpItem {
ip: v.ip.into(),
online: v.online,
is_direct: vnt_api.is_direct(&v.ip),
rtt,
}
})
.collect();
ResponsePayload::ClientIps(ClientIpList { items })
}
IpcCmd::ClientList(_) => {
let client_list = vnt_api.server_rpc().client_list().await?;
let key_sign = vnt_api.get_config().and_then(|config| config.key_sign());
let items = client_list
.list
.into_iter()
.map(|v| {
let ip = Ipv4Addr::from(v.ip);
let rtt = if let Some(v) = vnt_api.find_route(&ip) {
Some(v.rtt())
} else {
vnt_api.server_node_rtt(&ip).map(|v| v * 2)
};
let packet_loss = vnt_api.packet_loss_info(&ip).map(|info| PacketLoss {
sent: info.sent,
received: info.received,
loss_rate: info.loss_rate,
});
ClientInfoItem {
ip: v.ip,
name: v.name,
version: v.version,
online: v.online,
is_direct: vnt_api.is_direct(&Ipv4Addr::from(v.ip)),
last_connected_time: v.last_connected_time,
rtt,
key_equal: key_sign == v.key_sign,
packet_loss,
}
})
.collect();
ResponsePayload::ClientList(ClientInfoList { items })
}
IpcCmd::AllRoute(_) => {
let route_list = all_route(&vnt_api);
ResponsePayload::AllRoute(route_list)
}
};
let rs = IpcResponse {
response_payload: Some(rs),
};
framed.send(rs.encode_to_vec().into()).await?;
}
Ok(())
}
fn app_info(vnt_api: &VntApi) -> AppInfo {
let config = vnt_api.get_config();
let ips = vnt_api.client_ips();
let online_client_num = ips.iter().filter(|v| v.online).count() as _;
let offline_client_num = ips.iter().filter(|v| !v.online).count() as _;
let direct_client_num = ips.iter().filter(|ip| vnt_api.is_direct(&ip.ip)).count() as _;
let server_node_list = vnt_api.server_node_list();
let nat_info = vnt_api.nat_info();
AppInfo {
server_info: server_node_list
.into_iter()
.map(|v| ServerInfo {
server: v.server_addr.to_string(),
connected: v.connected,
server_rtt: v.rtt,
last_connected_time: v.last_connected_time,
})
.collect(),
name: config
.as_ref()
.map(|v| v.device_name.clone())
.unwrap_or_default(),
version: env!("CARGO_PKG_VERSION").to_string(),
ip: vnt_api.network().map(|v| v.ip.into()),
online_client_num,
offline_client_num,
direct_client_num,
device_id: config.map(|v| v.device_id.clone()).unwrap_or_default(),
nat_type: nat_info.as_ref().map(|v| format!("{:?}", v.nat_type)),
public_ipv4s: nat_info
.as_ref()
.map(|v| {
v.public_ips
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
})
.unwrap_or_default(),
public_ipv6: nat_info
.as_ref()
.and_then(|v| v.ipv6.map(|v| v.to_string())),
}
}
fn all_route(vnt_api: &VntApi) -> ClientRouteList {
let vec = vnt_api.route_table();
let mut items = Vec::with_capacity(vec.len());
for (ip, route_list) in vec {
items.push(ClientRouteItem {
ip: ip.into(),
route_list: route_list
.into_iter()
.map(|v| Route {
addr: v.route_key().to_string(),
metric: v.metric() as _,
rtt: v.rtt(),
})
.collect(),
})
}
ClientRouteList { items }
}
pub async fn run_server(bind_port: Option<u16>, vnt_api: VntApi) -> anyhow::Result<()> {
let mut port = bind_port.unwrap_or(DEFAULT_PORT);
let listener;
loop {
match TcpListener::bind(format!("127.0.0.1:{}", port)).await {
Ok(l) => {
listener = l;
break;
}
Err(e) if bind_port.is_none() && e.kind() == io::ErrorKind::AddrInUse => {
port = 0;
}
Err(e) => bail!("bind :{e:?}"),
}
}
let bound_addr = listener.local_addr()?;
log::info!("IPC Listening on {}", bound_addr);
let actual_port = bound_addr.port();
let path = get_port_file_path();
fs::write(&path, actual_port.to_string())?;
loop {
let (stream, peer_addr) = listener.accept().await?;
log::info!("IPC Connection from {}", peer_addr);
let vnt_api = vnt_api.clone();
tokio::spawn(async move {
if let Err(e) = handle_connection(stream, vnt_api).await {
log::warn!("IPC Error handling client: {:?},peer_addr={peer_addr}", e);
}
});
}
}