feat/web (Patchset 2) (#444)

This patch implement a restful server without any auth.

usage:

```bash
# run easytier-web, which acts as an gateway and registry for all easytier-core
$> easytier-web

# run easytier-core and connect to easytier-web with a token
$> easytier-core --config-server udp://127.0.0.1:22020/fdsafdsa

# use restful api to list session
$> curl -H "Content-Type: application/json" -X GET 127.0.0.1:11211/api/v1/sessions
[{"token":"fdsafdsa","client_url":"udp://127.0.0.1:48915","machine_id":"de3f5b8f-0f2f-d9d0-fb30-a2ac8951d92f"}]%

# use restful api to run a network instance
$> curl -H "Content-Type: application/json" -X POST 127.0.0.1:11211/api/v1/network/de3f5b8f-0f2f-d9d0-fb30-a2ac8951d92f -d '{"config": "listeners = [\"udp://0.0.0.0:12344\"]"}'

# use restful api to get network instance info
$> curl -H "Content-Type: application/json" -X GET 127.0.0.1:11211/api/v1/network/de3f5b8f-0f2f-d9d0-fb30-a2ac8951d92f/65437e50-b286-4098-a624-74429f2cb839 
```
This commit is contained in:
Sijie.Sun
2024-10-26 00:04:22 +08:00
committed by GitHub
parent b5c3726e67
commit a78b759741
33 changed files with 1539 additions and 263 deletions
+2 -6
View File
@@ -231,12 +231,8 @@ impl Default for TomlConfigLoader {
impl TomlConfigLoader {
pub fn new_from_str(config_str: &str) -> Result<Self, anyhow::Error> {
let mut config = toml::de::from_str::<Config>(config_str).with_context(|| {
format!(
"failed to parse config file: {}\n{}",
config_str, config_str
)
})?;
let mut config = toml::de::from_str::<Config>(config_str)
.with_context(|| format!("failed to parse config file: {}", config_str))?;
config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default()));
+30
View File
@@ -80,6 +80,36 @@ pub fn join_joinset_background<T: Debug + Send + Sync + 'static>(
);
}
pub fn get_machine_id() -> uuid::Uuid {
// TODO: load from local file
#[cfg(any(
target_os = "linux",
target_os = "macos",
target_os = "windows",
target_os = "freebsd"
))]
let gen_mid = machine_uid::get()
.map(|x| {
let mut b = [0u8; 16];
crate::tunnel::generate_digest_from_str("", x.as_str(), &mut b);
uuid::Uuid::from_bytes(b)
})
.unwrap_or(uuid::Uuid::new_v4());
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "windows",
target_os = "freebsd"
)))]
let gen_mid = uuid::Uuid::new_v4();
// TODO: save to local file
gen_mid
}
#[cfg(test)]
mod tests {
use super::*;
@@ -318,7 +318,7 @@ impl PunchSymToConeHoleClient {
let punch_random = self.punch_randomly.load(Ordering::Relaxed);
let punch_predicable = self.punch_predicablely.load(Ordering::Relaxed);
let scoped_punch_task: ScopedTask<Option<u32>> = tokio::spawn(async move {
if punch_predicable {
if punch_predicable && base_port_for_easy_sym.is_some() {
if let Some(inc) = my_nat_info.get_inc_of_easy_sym() {
let req = SendPunchPacketEasySymRequest {
listener_mapped_addr: remote_mapped_addr.clone().into(),
+52 -57
View File
@@ -7,14 +7,18 @@ use tabled::settings::Style;
use tokio::time::timeout;
use easytier::{
common::{constants::EASYTIER_VERSION, stun::StunInfoCollector, stun::StunInfoCollectorTrait},
common::{
constants::EASYTIER_VERSION,
stun::{StunInfoCollector, StunInfoCollectorTrait},
},
proto::{
cli::{
ConnectorManageRpc, ConnectorManageRpcClientFactory, DumpRouteRequest,
GetVpnPortalInfoRequest, ListConnectorRequest, ListForeignNetworkRequest,
ListGlobalForeignNetworkRequest, ListPeerRequest, ListPeerResponse, ListRouteRequest,
ListRouteResponse, NodeInfo, PeerManageRpc, PeerManageRpcClientFactory,
ShowNodeInfoRequest, VpnPortalRpc, VpnPortalRpcClientFactory,
list_peer_route_pair, ConnectorManageRpc, ConnectorManageRpcClientFactory,
DumpRouteRequest, GetVpnPortalInfoRequest, ListConnectorRequest,
ListForeignNetworkRequest, ListGlobalForeignNetworkRequest, ListPeerRequest,
ListPeerResponse, ListRouteRequest, ListRouteResponse, NodeInfo, PeerManageRpc,
PeerManageRpcClientFactory, ShowNodeInfoRequest, VpnPortalRpc,
VpnPortalRpcClientFactory,
},
common::NatType,
peer_rpc::{GetGlobalPeerMapRequest, PeerCenterRpc, PeerCenterRpcClientFactory},
@@ -22,7 +26,7 @@ use easytier::{
rpc_types::controller::BaseController,
},
tunnel::tcp::TcpTunnelConnector,
utils::{cost_to_str, float_to_str, list_peer_route_pair, PeerRoutePair},
utils::{cost_to_str, float_to_str, PeerRoutePair},
};
#[derive(Parser, Debug)]
@@ -222,25 +226,26 @@ impl CommandHandler {
impl From<PeerRoutePair> for PeerTableItem {
fn from(p: PeerRoutePair) -> Self {
let route = p.route.clone().unwrap_or_default();
PeerTableItem {
ipv4: p
.route
.ipv4_addr
.map(|ip| ip.to_string())
.unwrap_or_default(),
hostname: p.route.hostname.clone(),
cost: cost_to_str(p.route.cost),
ipv4: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(),
hostname: route.hostname.clone(),
cost: cost_to_str(route.cost),
lat_ms: float_to_str(p.get_latency_ms().unwrap_or(0.0), 3),
loss_rate: float_to_str(p.get_loss_rate().unwrap_or(0.0), 3),
rx_bytes: format_size(p.get_rx_bytes().unwrap_or(0), humansize::DECIMAL),
tx_bytes: format_size(p.get_tx_bytes().unwrap_or(0), humansize::DECIMAL),
tunnel_proto: p.get_conn_protos().unwrap_or_default().join(",").to_string(),
tunnel_proto: p
.get_conn_protos()
.unwrap_or_default()
.join(",")
.to_string(),
nat_type: p.get_udp_nat_type(),
id: p.route.peer_id.to_string(),
version: if p.route.version.is_empty() {
id: route.peer_id.to_string(),
version: if route.version.is_empty() {
"unknown".to_string()
} else {
p.route.version.to_string()
route.version.to_string()
},
}
}
@@ -287,10 +292,7 @@ impl CommandHandler {
items.push(p.into());
}
println!(
"{}",
tabled::Table::new(items).with(Style::modern())
);
println!("{}", tabled::Table::new(items).with(Style::modern()));
Ok(())
}
@@ -404,62 +406,59 @@ impl CommandHandler {
});
let peer_routes = self.list_peer_route_pair().await?;
for p in peer_routes.iter() {
let Some(next_hop_pair) = peer_routes
.iter()
.find(|pair| pair.route.peer_id == p.route.next_hop_peer_id)
else {
let Some(next_hop_pair) = peer_routes.iter().find(|pair| {
pair.route.clone().unwrap_or_default().peer_id
== p.route.clone().unwrap_or_default().next_hop_peer_id
}) else {
continue;
};
if p.route.cost == 1 {
let route = p.route.clone().unwrap_or_default();
if route.cost == 1 {
items.push(RouteTableItem {
ipv4: p
.route
.ipv4_addr
.map(|ip| ip.to_string())
.unwrap_or_default(),
hostname: p.route.hostname.clone(),
proxy_cidrs: p.route.proxy_cidrs.clone().join(",").to_string(),
ipv4: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(),
hostname: route.hostname.clone(),
proxy_cidrs: route.proxy_cidrs.clone().join(",").to_string(),
next_hop_ipv4: "DIRECT".to_string(),
next_hop_hostname: "".to_string(),
next_hop_lat: next_hop_pair.get_latency_ms().unwrap_or(0.0),
cost: p.route.cost,
version: if p.route.version.is_empty() {
cost: route.cost,
version: if route.version.is_empty() {
"unknown".to_string()
} else {
p.route.version.to_string()
route.version.to_string()
},
});
} else {
items.push(RouteTableItem {
ipv4: p
.route
.ipv4_addr
.map(|ip| ip.to_string())
.unwrap_or_default(),
hostname: p.route.hostname.clone(),
proxy_cidrs: p.route.proxy_cidrs.clone().join(",").to_string(),
ipv4: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(),
hostname: route.hostname.clone(),
proxy_cidrs: route.proxy_cidrs.clone().join(",").to_string(),
next_hop_ipv4: next_hop_pair
.route
.clone()
.unwrap_or_default()
.ipv4_addr
.map(|ip| ip.to_string())
.unwrap_or_default(),
next_hop_hostname: next_hop_pair.route.hostname.clone(),
next_hop_hostname: next_hop_pair
.route
.clone()
.unwrap_or_default()
.hostname
.clone(),
next_hop_lat: next_hop_pair.get_latency_ms().unwrap_or(0.0),
cost: p.route.cost,
version: if p.route.version.is_empty() {
cost: route.cost,
version: if route.version.is_empty() {
"unknown".to_string()
} else {
p.route.version.to_string()
route.version.to_string()
},
});
}
}
println!(
"{}",
tabled::Table::new(items).with(Style::modern())
);
println!("{}", tabled::Table::new(items).with(Style::modern()));
Ok(())
}
@@ -576,11 +575,7 @@ async fn main() -> Result<(), Error> {
});
}
println!(
"{}",
tabled::Table::new(table_rows)
.with(Style::modern())
);
println!("{}", tabled::Table::new(table_rows).with(Style::modern()));
}
SubCommand::VpnPortal => {
let vpn_portal_client = handler.get_vpn_portal_client().await?;
+48 -2
View File
@@ -1,3 +1,5 @@
#![allow(dead_code)]
#[macro_use]
extern crate rust_i18n;
@@ -21,7 +23,9 @@ use easytier::{
scoped_task::ScopedTask,
},
launcher, proto,
tunnel::udp::UdpTunnelConnector,
utils::{init_logger, setup_panic_handler},
web_client,
};
#[cfg(feature = "mimalloc")]
@@ -34,6 +38,13 @@ static GLOBAL_MIMALLOC: GlobalMiMalloc = GlobalMiMalloc;
#[derive(Parser, Debug)]
#[command(name = "easytier-core", author, version = EASYTIER_VERSION , about, long_about = None)]
struct Cli {
#[arg(
short = 'w',
long,
help = t!("core_clap.config_server").to_string()
)]
config_server: Option<String>,
#[arg(
short,
long,
@@ -640,12 +651,47 @@ pub fn handle_event(mut events: EventBusSubscriber) -> tokio::task::JoinHandle<(
#[tokio::main]
async fn main() {
setup_panic_handler();
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
rust_i18n::set_locale(&locale);
let cli = Cli::parse();
setup_panic_handler();
if cli.config_server.is_some() {
let config_server_url_s = cli.config_server.clone().unwrap();
let config_server_url = match url::Url::parse(&config_server_url_s) {
Ok(u) => u,
Err(_) => format!(
"udp://config-server.easytier.top:22020/{}",
config_server_url_s
)
.parse()
.unwrap(),
};
let mut c_url = config_server_url.clone();
c_url.set_path("");
let token = config_server_url
.path_segments()
.and_then(|mut x| x.next())
.map(|x| x.to_string())
.unwrap_or_default();
println!(
"Entering config client mode...\n server: {}\n token: {}",
c_url, token,
);
if token.is_empty() {
panic!("empty token");
}
let _wc = web_client::WebClient::new(UdpTunnelConnector::new(c_url), token.to_string());
tokio::signal::ctrl_c().await.unwrap();
return;
}
let cfg = TomlConfigLoader::from(cli);
init_logger(&cfg, false).unwrap();
+15 -40
View File
@@ -12,27 +12,12 @@ use crate::{
},
instance::instance::Instance,
peers::rpc_service::PeerManagerRpcService,
proto::{
cli::{PeerInfo, Route},
common::StunInfo,
peer_rpc::GetIpListResponse,
},
utils::{list_peer_route_pair, PeerRoutePair},
proto::cli::{list_peer_route_pair, PeerInfo, Route},
};
use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use tokio::{sync::broadcast, task::JoinSet};
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
pub struct MyNodeInfo {
pub virtual_ipv4: String,
pub hostname: String,
pub version: String,
pub ips: GetIpListResponse,
pub stun_info: StunInfo,
pub listeners: Vec<String>,
pub vpn_portal_cfg: Option<String>,
}
pub type MyNodeInfo = crate::proto::web::MyNodeInfo;
struct EasyTierData {
events: RwLock<VecDeque<(DateTime<Local>, GlobalCtxEvent)>>,
@@ -164,18 +149,15 @@ impl EasyTierLauncher {
global_ctx_c.get_flags().dev_name.clone();
let node_info = MyNodeInfo {
virtual_ipv4: global_ctx_c
.get_ipv4()
.map(|x| x.to_string())
.unwrap_or_default(),
virtual_ipv4: global_ctx_c.get_ipv4().map(|x| x.address().into()),
hostname: global_ctx_c.get_hostname(),
version: EASYTIER_VERSION.to_string(),
ips: global_ctx_c.get_ip_collector().collect_ip_addrs().await,
stun_info: global_ctx_c.get_stun_info_collector().get_stun_info(),
ips: Some(global_ctx_c.get_ip_collector().collect_ip_addrs().await),
stun_info: Some(global_ctx_c.get_stun_info_collector().get_stun_info()),
listeners: global_ctx_c
.get_running_listeners()
.iter()
.map(|x| x.to_string())
.into_iter()
.map(Into::into)
.collect(),
vpn_portal_cfg: Some(
vpn_portal
@@ -311,18 +293,7 @@ impl Drop for EasyTierLauncher {
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct NetworkInstanceRunningInfo {
pub dev_name: String,
pub my_node_info: MyNodeInfo,
pub events: Vec<(DateTime<Local>, GlobalCtxEvent)>,
pub node_info: MyNodeInfo,
pub routes: Vec<Route>,
pub peers: Vec<PeerInfo>,
pub peer_route_pairs: Vec<PeerRoutePair>,
pub running: bool,
pub error_msg: Option<String>,
}
pub type NetworkInstanceRunningInfo = crate::proto::web::NetworkInstanceRunningInfo;
pub struct NetworkInstance {
config: TomlConfigLoader,
@@ -362,9 +333,13 @@ impl NetworkInstance {
Some(NetworkInstanceRunningInfo {
dev_name: launcher.get_dev_name(),
my_node_info: launcher.get_node_info(),
events: launcher.get_events(),
node_info: launcher.get_node_info(),
my_node_info: Some(launcher.get_node_info()),
events: launcher
.get_events()
.iter()
.map(|(t, e)| (t.to_string(), format!("{:?}", e)))
.collect(),
node_info: Some(launcher.get_node_info()),
routes,
peers,
peer_route_pairs,
+1
View File
@@ -13,6 +13,7 @@ pub mod peers;
pub mod proto;
pub mod tunnel;
pub mod utils;
pub mod web_client;
#[cfg(test)]
mod tests;
@@ -353,12 +353,13 @@ impl ForeignNetworkManagerData {
}
async fn clear_no_conn_peer(&self, network_name: &String) {
let peer_map = self
let Some(peer_map) = self
.network_peer_maps
.get(network_name)
.unwrap()
.peer_map
.clone();
.and_then(|v| Some(v.peer_map.clone()))
else {
return;
};
peer_map.clean_peer_without_conn().await;
}
+5
View File
@@ -56,6 +56,11 @@ message Route {
common.PeerFeatureFlag feature_flag = 10;
}
message PeerRoutePair {
Route route = 1;
PeerInfo peer = 2;
}
message NodeInfo {
uint32 peer_id = 1;
string ipv4_addr = 2;
+112
View File
@@ -1 +1,113 @@
include!(concat!(env!("OUT_DIR"), "/cli.rs"));
impl PeerRoutePair {
pub fn get_latency_ms(&self) -> Option<f64> {
let mut ret = u64::MAX;
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(stats) = &conn.stats else {
continue;
};
ret = ret.min(stats.latency_us);
}
if ret == u64::MAX {
None
} else {
Some(f64::from(ret as u32) / 1000.0)
}
}
pub fn get_rx_bytes(&self) -> Option<u64> {
let mut ret = 0;
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(stats) = &conn.stats else {
continue;
};
ret += stats.rx_bytes;
}
if ret == 0 {
None
} else {
Some(ret)
}
}
pub fn get_tx_bytes(&self) -> Option<u64> {
let mut ret = 0;
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(stats) = &conn.stats else {
continue;
};
ret += stats.tx_bytes;
}
if ret == 0 {
None
} else {
Some(ret)
}
}
pub fn get_loss_rate(&self) -> Option<f64> {
let mut ret = 0.0;
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
ret += conn.loss_rate;
}
if ret == 0.0 {
None
} else {
Some(ret as f64)
}
}
pub fn get_conn_protos(&self) -> Option<Vec<String>> {
let mut ret = vec![];
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(tunnel_info) = &conn.tunnel else {
continue;
};
// insert if not exists
if !ret.contains(&tunnel_info.tunnel_type) {
ret.push(tunnel_info.tunnel_type.clone());
}
}
if ret.is_empty() {
None
} else {
Some(ret)
}
}
pub fn get_udp_nat_type(self: &Self) -> String {
use crate::proto::common::NatType;
let mut ret = NatType::Unknown;
if let Some(r) = &self.route.clone().unwrap_or_default().stun_info {
ret = NatType::try_from(r.udp_nat_type).unwrap();
}
format!("{:?}", ret)
}
}
pub fn list_peer_route_pair(peers: Vec<PeerInfo>, routes: Vec<Route>) -> Vec<PeerRoutePair> {
let mut pairs: Vec<PeerRoutePair> = vec![];
for route in routes.iter() {
let peer = peers.iter().find(|peer| peer.peer_id == route.peer_id);
let pair = PeerRoutePair {
route: Some(route.clone()),
peer: peer.cloned(),
};
pairs.push(pair);
}
pairs
}
+1 -1
View File
@@ -21,7 +21,7 @@ message MalformatRpcPacket { string error_message = 1; }
message Timeout { string error_message = 1; }
message Error {
oneof error {
oneof error_kind {
OtherError other_error = 1;
InvalidMethodIndex invalid_method_index = 2;
InvalidService invalid_service = 3;
+17 -17
View File
@@ -6,44 +6,44 @@ include!(concat!(env!("OUT_DIR"), "/error.rs"));
impl From<&rpc_types::error::Error> for Error {
fn from(e: &rpc_types::error::Error) -> Self {
use super::error::error::Error as ProtoError;
use super::error::error::ErrorKind as ProtoError;
match e {
rpc_types::error::Error::ExecutionError(e) => Self {
error: Some(ProtoError::ExecuteError(ExecuteError {
error_message: e.to_string(),
error_kind: Some(ProtoError::ExecuteError(ExecuteError {
error_message: format!("{:?}", e),
})),
},
rpc_types::error::Error::DecodeError(_) => Self {
error: Some(ProtoError::ProstDecodeError(ProstDecodeError {})),
error_kind: Some(ProtoError::ProstDecodeError(ProstDecodeError {})),
},
rpc_types::error::Error::EncodeError(_) => Self {
error: Some(ProtoError::ProstEncodeError(ProstEncodeError {})),
error_kind: Some(ProtoError::ProstEncodeError(ProstEncodeError {})),
},
rpc_types::error::Error::InvalidMethodIndex(m, s) => Self {
error: Some(ProtoError::InvalidMethodIndex(InvalidMethodIndex {
error_kind: Some(ProtoError::InvalidMethodIndex(InvalidMethodIndex {
method_index: *m as u32,
service_name: s.to_string(),
service_name: format!("{:?}", s),
})),
},
rpc_types::error::Error::InvalidServiceKey(s, _) => Self {
error: Some(ProtoError::InvalidService(InvalidService {
service_name: s.to_string(),
error_kind: Some(ProtoError::InvalidService(InvalidService {
service_name: format!("{:?}", s),
})),
},
rpc_types::error::Error::MalformatRpcPacket(e) => Self {
error: Some(ProtoError::MalformatRpcPacket(MalformatRpcPacket {
error_message: e.to_string(),
error_kind: Some(ProtoError::MalformatRpcPacket(MalformatRpcPacket {
error_message: format!("{:?}", e),
})),
},
rpc_types::error::Error::Timeout(e) => Self {
error: Some(ProtoError::Timeout(Timeout {
error_message: e.to_string(),
error_kind: Some(ProtoError::Timeout(Timeout {
error_message: format!("{:?}", e),
})),
},
#[allow(unreachable_patterns)]
e => Self {
error: Some(ProtoError::OtherError(OtherError {
error_message: e.to_string(),
error_kind: Some(ProtoError::OtherError(OtherError {
error_message: format!("{:?}", e),
})),
},
}
@@ -52,8 +52,8 @@ impl From<&rpc_types::error::Error> for Error {
impl From<&Error> for rpc_types::error::Error {
fn from(e: &Error) -> Self {
use super::error::error::Error as ProtoError;
match &e.error {
use super::error::error::ErrorKind as ProtoError;
match &e.error_kind {
Some(ProtoError::ExecuteError(e)) => {
Self::ExecutionError(anyhow::anyhow!(e.error_message.clone()))
}
+1
View File
@@ -5,6 +5,7 @@ pub mod cli;
pub mod common;
pub mod error;
pub mod peer_rpc;
pub mod web;
#[cfg(test)]
pub mod tests;
+18 -1
View File
@@ -1,9 +1,10 @@
use std::sync::{Arc, Mutex};
use std::sync::{atomic::AtomicBool, Arc, Mutex};
use futures::{SinkExt as _, StreamExt};
use tokio::{task::JoinSet, time::timeout};
use crate::{
defer,
proto::rpc_types::error::Error,
tunnel::{packet_def::PacketType, ring::create_ring_tunnel_pair, Tunnel},
};
@@ -17,6 +18,7 @@ pub struct BidirectRpcManager {
rx_timeout: Option<std::time::Duration>,
error: Arc<Mutex<Option<Error>>>,
tunnel: Mutex<Option<Box<dyn Tunnel>>>,
running: Arc<AtomicBool>,
tasks: Mutex<Option<JoinSet<()>>>,
}
@@ -30,6 +32,7 @@ impl BidirectRpcManager {
rx_timeout: None,
error: Arc::new(Mutex::new(None)),
tunnel: Mutex::new(None),
running: Arc::new(AtomicBool::new(false)),
tasks: Mutex::new(None),
}
@@ -50,6 +53,8 @@ impl BidirectRpcManager {
let mut tasks = JoinSet::new();
self.rpc_client.run();
self.rpc_server.run();
self.running
.store(true, std::sync::atomic::Ordering::Relaxed);
let (server_tx, mut server_rx) = (
self.rpc_server.get_transport_sink(),
@@ -64,7 +69,11 @@ impl BidirectRpcManager {
self.tunnel.lock().unwrap().replace(inner);
let e_clone = self.error.clone();
let r_clone = self.running.clone();
tasks.spawn(async move {
defer! {
r_clone.store(false, std::sync::atomic::Ordering::Relaxed);
}
loop {
let packet = tokio::select! {
Some(Ok(packet)) = server_rx.next() => {
@@ -90,7 +99,11 @@ impl BidirectRpcManager {
let recv_timeout = self.rx_timeout;
let e_clone = self.error.clone();
let r_clone = self.running.clone();
tasks.spawn(async move {
defer! {
r_clone.store(false, std::sync::atomic::Ordering::Relaxed);
}
loop {
let ret = if let Some(recv_timeout) = recv_timeout {
match timeout(recv_timeout, inner_rx.next()).await {
@@ -161,4 +174,8 @@ impl BidirectRpcManager {
tasks.abort_all();
}
}
pub fn is_running(&self) -> bool {
self.running.load(std::sync::atomic::Ordering::Relaxed)
}
}
+6
View File
@@ -307,6 +307,7 @@ async fn test_bidirect_rpc_manager() {
use crate::proto::rpc_impl::bidirect::BidirectRpcManager;
use crate::tunnel::tcp::{TcpTunnelConnector, TcpTunnelListener};
use crate::tunnel::{TunnelConnector, TunnelListener};
use tokio::sync::Notify;
let c = BidirectRpcManager::new();
let s = BidirectRpcManager::new();
@@ -323,6 +324,8 @@ async fn test_bidirect_rpc_manager() {
});
s.rpc_server().registry().register(service, "test");
let server_test_done = Arc::new(Notify::new());
let server_test_done_clone = server_test_done.clone();
let mut tcp_listener = TcpTunnelListener::new("tcp://0.0.0.0:55443".parse().unwrap());
let s_task: ScopedTask<()> = tokio::spawn(async move {
tcp_listener.listen().await.unwrap();
@@ -344,6 +347,8 @@ async fn test_bidirect_rpc_manager() {
assert_eq!(ret.greeting, "Hello Client world!");
println!("server done, {:?}", ret);
server_test_done_clone.notify_one();
s.wait().await;
})
.into();
@@ -369,6 +374,7 @@ async fn test_bidirect_rpc_manager() {
assert_eq!(ret.greeting, "Hello Server world!");
println!("client done, {:?}", ret);
server_test_done.notified().await;
drop(c);
s_task.await.unwrap();
}
+100
View File
@@ -0,0 +1,100 @@
syntax = "proto3";
import "common.proto";
import "peer_rpc.proto";
import "cli.proto";
package web;
message MyNodeInfo {
common.Ipv4Addr virtual_ipv4 = 1;
string hostname = 2;
string version = 3;
peer_rpc.GetIpListResponse ips = 4;
common.StunInfo stun_info = 5;
repeated common.Url listeners = 6;
optional string vpn_portal_cfg = 7;
}
message NetworkInstanceRunningInfo {
string dev_name = 1;
MyNodeInfo my_node_info = 2;
map<string, string> events = 3;
MyNodeInfo node_info = 4;
repeated cli.Route routes = 5;
repeated cli.PeerInfo peers = 6;
repeated cli.PeerRoutePair peer_route_pairs = 7;
bool running = 8;
optional string error_msg = 9;
}
message NetworkInstanceRunningInfoMap {
map<string, NetworkInstanceRunningInfo> map = 1;
}
message HeartbeatRequest {
common.UUID machine_id = 1;
common.UUID inst_id = 2;
string user_token = 3;
}
message HeartbeatResponse {
}
service WebServerService {
rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse) {}
}
message ValidateConfigRequest {
string config = 1;
}
message ValidateConfigResponse {
}
message RunNetworkInstanceRequest {
string config = 1;
}
message RunNetworkInstanceResponse {
}
message RetainNetworkInstanceRequest {
repeated common.UUID inst_ids = 1;
}
message RetainNetworkInstanceResponse {
repeated common.UUID remain_inst_ids = 1;
}
message CollectNetworkInfoRequest {
repeated common.UUID inst_ids = 1;
}
message CollectNetworkInfoResponse {
NetworkInstanceRunningInfoMap info = 1;
}
message ListNetworkInstanceRequest {
}
message ListNetworkInstanceResponse {
repeated common.UUID inst_ids = 1;
}
message DeleteNetworkInstanceRequest {
repeated common.UUID inst_ids = 1;
}
message DeleteNetworkInstanceResponse {
repeated common.UUID remain_inst_ids = 1;
}
service WebClientService {
rpc ValidateConfig(ValidateConfigRequest) returns (ValidateConfigResponse) {}
rpc RunNetworkInstance(RunNetworkInstanceRequest) returns (RunNetworkInstanceResponse) {}
rpc RetainNetworkInstance(RetainNetworkInstanceRequest) returns (RetainNetworkInstanceResponse) {}
rpc CollectNetworkInfo(CollectNetworkInfoRequest) returns (CollectNetworkInfoResponse) {}
rpc ListNetworkInstance(ListNetworkInstanceRequest) returns (ListNetworkInstanceResponse) {}
rpc DeleteNetworkInstance(DeleteNetworkInstanceRequest) returns (DeleteNetworkInstanceResponse) {}
}
+1
View File
@@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/web.rs"));
+3 -125
View File
@@ -1,132 +1,10 @@
use anyhow::Context;
use serde::{Deserialize, Serialize};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
use crate::{
common::{config::ConfigLoader, get_logger_timer_rfc3339},
proto::{
cli::{PeerInfo, Route},
common::NatType,
},
};
use crate::common::{config::ConfigLoader, get_logger_timer_rfc3339};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerRoutePair {
pub route: Route,
pub peer: Option<PeerInfo>,
}
impl PeerRoutePair {
pub fn get_latency_ms(&self) -> Option<f64> {
let mut ret = u64::MAX;
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(stats) = &conn.stats else {
continue;
};
ret = ret.min(stats.latency_us);
}
if ret == u64::MAX {
None
} else {
Some(f64::from(ret as u32) / 1000.0)
}
}
pub fn get_rx_bytes(&self) -> Option<u64> {
let mut ret = 0;
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(stats) = &conn.stats else {
continue;
};
ret += stats.rx_bytes;
}
if ret == 0 {
None
} else {
Some(ret)
}
}
pub fn get_tx_bytes(&self) -> Option<u64> {
let mut ret = 0;
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(stats) = &conn.stats else {
continue;
};
ret += stats.tx_bytes;
}
if ret == 0 {
None
} else {
Some(ret)
}
}
pub fn get_loss_rate(&self) -> Option<f64> {
let mut ret = 0.0;
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
ret += conn.loss_rate;
}
if ret == 0.0 {
None
} else {
Some(ret as f64)
}
}
pub fn get_conn_protos(&self) -> Option<Vec<String>> {
let mut ret = vec![];
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(tunnel_info) = &conn.tunnel else {
continue;
};
// insert if not exists
if !ret.contains(&tunnel_info.tunnel_type) {
ret.push(tunnel_info.tunnel_type.clone());
}
}
if ret.is_empty() {
None
} else {
Some(ret)
}
}
pub fn get_udp_nat_type(self: &Self) -> String {
let mut ret = NatType::Unknown;
if let Some(r) = &self.route.stun_info {
ret = NatType::try_from(r.udp_nat_type).unwrap();
}
format!("{:?}", ret)
}
}
pub fn list_peer_route_pair(peers: Vec<PeerInfo>, routes: Vec<Route>) -> Vec<PeerRoutePair> {
let mut pairs: Vec<PeerRoutePair> = vec![];
for route in routes.iter() {
let peer = peers.iter().find(|peer| peer.peer_id == route.peer_id);
let pair = PeerRoutePair {
route: route.clone(),
peer: peer.cloned(),
};
pairs.push(pair);
}
pairs
}
pub type PeerRoutePair = crate::proto::cli::PeerRoutePair;
pub fn cost_to_str(cost: i32) -> String {
if cost == 1 {
@@ -250,7 +128,7 @@ pub fn setup_panic_handler() {
use std::io::Write;
std::panic::set_hook(Box::new(|info| {
let backtrace = backtrace::Backtrace::force_capture();
println!("panic occurred: {:?}", info);
println!("panic occurred: {:?}, backtrace: {:#?}", info, backtrace);
let _ = std::fs::File::create("easytier-panic.log")
.and_then(|mut f| f.write_all(format!("{:?}\n{:#?}", info, backtrace).as_bytes()));
std::process::exit(1);
+171
View File
@@ -0,0 +1,171 @@
use std::collections::BTreeMap;
use dashmap::DashMap;
use crate::{
common::config::{ConfigLoader, TomlConfigLoader},
launcher::NetworkInstance,
proto::{
rpc_types::{self, controller::BaseController},
web::{
CollectNetworkInfoRequest, CollectNetworkInfoResponse, DeleteNetworkInstanceRequest,
DeleteNetworkInstanceResponse, ListNetworkInstanceRequest, ListNetworkInstanceResponse,
NetworkInstanceRunningInfoMap, RetainNetworkInstanceRequest,
RetainNetworkInstanceResponse, RunNetworkInstanceRequest, RunNetworkInstanceResponse,
ValidateConfigRequest, ValidateConfigResponse, WebClientService,
},
},
};
pub struct Controller {
token: String,
instance_map: DashMap<uuid::Uuid, NetworkInstance>,
}
impl Controller {
pub fn new(token: String) -> Self {
Controller {
token,
instance_map: DashMap::new(),
}
}
pub fn run_network_instance(&self, cfg: TomlConfigLoader) -> Result<(), anyhow::Error> {
let instance_id = cfg.get_id();
if self.instance_map.contains_key(&instance_id) {
anyhow::bail!("instance {} already exists", instance_id);
}
let mut instance = NetworkInstance::new(cfg);
instance.start()?;
println!("instance {} started", instance_id);
self.instance_map.insert(instance_id, instance);
Ok(())
}
pub fn retain_network_instance(
&self,
instance_ids: Vec<uuid::Uuid>,
) -> Result<RetainNetworkInstanceResponse, anyhow::Error> {
self.instance_map.retain(|k, _| instance_ids.contains(k));
let remain = self
.instance_map
.iter()
.map(|item| item.key().clone().into())
.collect::<Vec<_>>();
println!("instance {:?} retained", remain);
Ok(RetainNetworkInstanceResponse {
remain_inst_ids: remain,
})
}
pub fn collect_network_infos(&self) -> Result<NetworkInstanceRunningInfoMap, anyhow::Error> {
let mut map = BTreeMap::new();
for instance in self.instance_map.iter() {
if let Some(info) = instance.get_running_info() {
map.insert(instance.key().to_string(), info);
}
}
Ok(NetworkInstanceRunningInfoMap { map })
}
pub fn list_network_instance_ids(&self) -> Vec<uuid::Uuid> {
self.instance_map
.iter()
.map(|item| item.key().clone())
.collect()
}
pub fn token(&self) -> String {
self.token.clone()
}
}
#[async_trait::async_trait]
impl WebClientService for Controller {
type Controller = BaseController;
async fn validate_config(
&self,
_: BaseController,
req: ValidateConfigRequest,
) -> Result<ValidateConfigResponse, rpc_types::error::Error> {
let _ = TomlConfigLoader::new_from_str(&req.config)?;
Ok(ValidateConfigResponse {})
}
async fn run_network_instance(
&self,
_: BaseController,
req: RunNetworkInstanceRequest,
) -> Result<RunNetworkInstanceResponse, rpc_types::error::Error> {
let cfg = TomlConfigLoader::new_from_str(&req.config)?;
self.run_network_instance(cfg)?;
Ok(RunNetworkInstanceResponse {})
}
async fn retain_network_instance(
&self,
_: BaseController,
req: RetainNetworkInstanceRequest,
) -> Result<RetainNetworkInstanceResponse, rpc_types::error::Error> {
Ok(self.retain_network_instance(req.inst_ids.into_iter().map(Into::into).collect())?)
}
async fn collect_network_info(
&self,
_: BaseController,
req: CollectNetworkInfoRequest,
) -> Result<CollectNetworkInfoResponse, rpc_types::error::Error> {
let mut ret = self.collect_network_infos()?;
let include_inst_ids = req
.inst_ids
.iter()
.cloned()
.map(|id| id.to_string())
.collect::<Vec<_>>();
if !include_inst_ids.is_empty() {
let mut to_remove = Vec::new();
for (k, _) in ret.map.iter() {
if !include_inst_ids.contains(&k) {
to_remove.push(k.clone());
}
}
for k in to_remove {
ret.map.remove(&k);
}
}
Ok(CollectNetworkInfoResponse { info: Some(ret) })
}
// rpc ListNetworkInstance(ListNetworkInstanceRequest) returns (ListNetworkInstanceResponse) {}
async fn list_network_instance(
&self,
_: BaseController,
_: ListNetworkInstanceRequest,
) -> Result<ListNetworkInstanceResponse, rpc_types::error::Error> {
Ok(ListNetworkInstanceResponse {
inst_ids: self
.list_network_instance_ids()
.into_iter()
.map(Into::into)
.collect(),
})
}
// rpc DeleteNetworkInstance(DeleteNetworkInstanceRequest) returns (DeleteNetworkInstanceResponse) {}
async fn delete_network_instance(
&self,
_: BaseController,
req: DeleteNetworkInstanceRequest,
) -> Result<DeleteNetworkInstanceResponse, rpc_types::error::Error> {
let mut inst_ids = self.list_network_instance_ids();
inst_ids.retain(|id| !req.inst_ids.contains(&(id.clone().into())));
self.retain_network_instance(inst_ids.clone())?;
Ok(DeleteNetworkInstanceResponse {
remain_inst_ids: inst_ids.into_iter().map(Into::into).collect(),
})
}
}
+48
View File
@@ -0,0 +1,48 @@
use std::sync::Arc;
use crate::{common::scoped_task::ScopedTask, tunnel::TunnelConnector};
pub mod controller;
pub mod session;
pub struct WebClient {
controller: Arc<controller::Controller>,
tasks: ScopedTask<()>,
}
impl WebClient {
pub fn new<T: TunnelConnector + 'static, S: ToString>(connector: T, token: S) -> Self {
let controller = Arc::new(controller::Controller::new(token.to_string()));
let controller_clone = controller.clone();
let tasks = ScopedTask::from(tokio::spawn(async move {
Self::routine(controller_clone, Box::new(connector)).await;
}));
WebClient { controller, tasks }
}
async fn routine(
controller: Arc<controller::Controller>,
mut connector: Box<dyn TunnelConnector>,
) {
loop {
let conn = match connector.connect().await {
Ok(conn) => conn,
Err(e) => {
println!(
"Failed to connect to the server ({}), retrying in 5 seconds...",
e
);
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
continue;
}
};
println!("Successfully connected to {:?}", conn.info());
let mut session = session::Session::new(conn, controller.clone());
session.wait().await;
}
}
}
+126
View File
@@ -0,0 +1,126 @@
use std::sync::Arc;
use tokio::{
sync::{broadcast, Mutex},
task::JoinSet,
time::interval,
};
use crate::{
common::get_machine_id,
proto::{
rpc_impl::bidirect::BidirectRpcManager,
rpc_types::controller::BaseController,
web::{
HeartbeatRequest, HeartbeatResponse, WebClientServiceServer,
WebServerServiceClientFactory,
},
},
tunnel::Tunnel,
};
use super::controller::Controller;
#[derive(Debug, Clone)]
struct HeartbeatCtx {
notifier: Arc<broadcast::Sender<HeartbeatResponse>>,
resp: Arc<Mutex<Option<HeartbeatResponse>>>,
}
pub struct Session {
rpc_mgr: BidirectRpcManager,
controller: Arc<Controller>,
heartbeat_ctx: HeartbeatCtx,
tasks: Mutex<JoinSet<()>>,
}
impl Session {
pub fn new(tunnel: Box<dyn Tunnel>, controller: Arc<Controller>) -> Self {
let rpc_mgr = BidirectRpcManager::new();
rpc_mgr.run_with_tunnel(tunnel);
rpc_mgr
.rpc_server()
.registry()
.register(WebClientServiceServer::new(controller.clone()), "");
let mut tasks: JoinSet<()> = JoinSet::new();
let heartbeat_ctx = Self::heartbeat_routine(&rpc_mgr, controller.token(), &mut tasks);
Session {
rpc_mgr,
controller,
heartbeat_ctx,
tasks: Mutex::new(tasks),
}
}
fn heartbeat_routine(
rpc_mgr: &BidirectRpcManager,
token: String,
tasks: &mut JoinSet<()>,
) -> HeartbeatCtx {
let (tx, _rx1) = broadcast::channel(2);
let ctx = HeartbeatCtx {
notifier: Arc::new(tx),
resp: Arc::new(Mutex::new(None)),
};
let mid = get_machine_id();
let inst_id = uuid::Uuid::new_v4();
let token = token;
let ctx_clone = ctx.clone();
let mut tick = interval(std::time::Duration::from_secs(1));
let client = rpc_mgr
.rpc_client()
.scoped_client::<WebServerServiceClientFactory<BaseController>>(1, 1, "".to_string());
tasks.spawn(async move {
let req = HeartbeatRequest {
machine_id: Some(mid.into()),
inst_id: Some(inst_id.into()),
user_token: token.to_string(),
};
loop {
tick.tick().await;
match client
.heartbeat(BaseController::default(), req.clone())
.await
{
Err(e) => {
tracing::error!("heartbeat failed: {:?}", e);
break;
}
Ok(resp) => {
tracing::debug!("heartbeat response: {:?}", resp);
let _ = ctx_clone.notifier.send(resp.clone());
ctx_clone.resp.lock().await.replace(resp);
}
}
}
});
ctx
}
async fn wait_routines(&self) {
self.tasks.lock().await.join_next().await;
// if any task failed, we should abort all tasks
self.tasks.lock().await.abort_all();
}
pub async fn wait(&mut self) {
tokio::select! {
_ = self.rpc_mgr.wait() => {}
_ = self.wait_routines() => {}
}
}
pub async fn wait_next_heartbeat(&self) -> Option<HeartbeatResponse> {
let mut rx = self.heartbeat_ctx.notifier.subscribe();
rx.recv().await.ok()
}
}