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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1539 additions and 263 deletions

View File

@ -111,7 +111,7 @@ jobs:
run: |
bash ./.github/workflows/install_rust.sh
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips --package=easytier
else
cargo build --release --verbose --target $TARGET
fi
@ -182,6 +182,9 @@ jobs:
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
if [[ ! $TARGET =~ ^mips.*$ ]]; then
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./artifacts/objects/
fi
mv ./artifacts/objects/* ./artifacts/
rm -rf ./artifacts/objects/

128
Cargo.lock generated
View File

@ -428,6 +428,73 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "axum"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae"
dependencies = [
"async-trait",
"axum-core",
"axum-macros",
"bytes",
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.4.1",
"hyper-util",
"itoa 1.0.11",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper 1.0.1",
"tokio",
"tower 0.5.1",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper 1.0.1",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-macros"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
]
[[package]]
name = "backtrace"
version = "0.3.73"
@ -1312,9 +1379,9 @@ dependencies = [
[[package]]
name = "dashmap"
version = "6.0.1"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [
"cfg-if",
"crossbeam-utils",
@ -1574,6 +1641,7 @@ dependencies = [
"http 1.1.0",
"humansize",
"indexmap 1.9.3",
"machine-uid",
"mimalloc-rust",
"network-interface",
"nix 0.27.1",
@ -1678,7 +1746,18 @@ dependencies = [
name = "easytier-web"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"axum",
"clap",
"dashmap",
"easytier",
"serde",
"thiserror",
"tokio",
"tracing",
"url",
"uuid",
]
[[package]]
@ -2733,6 +2812,7 @@ dependencies = [
"http 1.1.0",
"http-body 1.0.1",
"httparse",
"httpdate",
"itoa 1.0.11",
"pin-project-lite",
"smallvec",
@ -2768,7 +2848,7 @@ dependencies = [
"pin-project-lite",
"socket2",
"tokio",
"tower",
"tower 0.4.13",
"tower-service",
"tracing",
]
@ -3258,6 +3338,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "machine-uid"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4506fa0abb0a2ea93f5862f55973da0a662d2ad0e98f337a1c5aac657f0892"
dependencies = [
"libc",
"winreg 0.52.0",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@ -3302,6 +3392,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "matchit"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "md5"
version = "0.7.0"
@ -5326,6 +5422,16 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
dependencies = [
"itoa 1.0.11",
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.19"
@ -6583,6 +6689,22 @@ dependencies = [
"tower-service",
]
[[package]]
name = "tower"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper 0.1.2",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"

View File

@ -1,7 +1,7 @@
[workspace]
resolver = "2"
members = ["easytier", "easytier-gui/src-tauri", "easytier-rpc-build", "easytier-web"]
default-members = ["easytier"]
default-members = ["easytier", "easytier-web"]
[profile.dev]
panic = "unwind"

View File

@ -11,7 +11,7 @@ export interface NetworkConfig {
dhcp: boolean
virtual_ipv4: string
network_length: number,
network_length: number
hostname?: string
network_name: string
network_secret: string

View File

@ -4,4 +4,25 @@ version = "0.1.0"
edition = "2021"
[dependencies]
easytier = { path = "../easytier" }
easytier = { path = "../easytier" }
tracing = { version = "0.1", features = ["log"] }
anyhow = { version = "1.0" }
thiserror = "1.0"
tokio = { version = "1", features = ["full"] }
dashmap = "6.1"
url = "2.2"
async-trait = "0.1"
axum = { version = "0.7", features = ["macros"] }
clap = { version = "4.4.8", features = [
"string",
"unicode",
"derive",
"wrap_help",
] }
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1.5.0", features = [
"v4",
"fast-rng",
"macro-diagnostics",
"serde",
] }

View File

@ -0,0 +1,134 @@
pub mod session;
pub mod storage;
use std::sync::Arc;
use dashmap::DashMap;
use easytier::{common::scoped_task::ScopedTask, tunnel::TunnelListener};
use session::Session;
use storage::{Storage, StorageToken};
#[derive(Debug)]
pub struct ClientManager {
accept_task: Option<ScopedTask<()>>,
clear_task: Option<ScopedTask<()>>,
client_sessions: Arc<DashMap<url::Url, Arc<Session>>>,
storage: Storage,
}
impl ClientManager {
pub fn new() -> Self {
ClientManager {
accept_task: None,
clear_task: None,
client_sessions: Arc::new(DashMap::new()),
storage: Storage::new(),
}
}
pub async fn serve<L: TunnelListener + 'static>(
&mut self,
mut listener: L,
) -> Result<(), anyhow::Error> {
listener.listen().await?;
let sessions = self.client_sessions.clone();
let storage = self.storage.weak_ref();
let task = tokio::spawn(async move {
while let Ok(tunnel) = listener.accept().await {
let info = tunnel.info().unwrap();
let client_url: url::Url = info.remote_addr.unwrap().into();
println!("New session from {:?}", tunnel.info());
let session = Session::new(tunnel, storage.clone(), client_url.clone());
sessions.insert(client_url, Arc::new(session));
}
});
self.accept_task = Some(ScopedTask::from(task));
let sessions = self.client_sessions.clone();
let task = tokio::spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(15)).await;
sessions.retain(|_, session| session.is_running());
}
});
self.clear_task = Some(ScopedTask::from(task));
Ok(())
}
pub fn is_running(&self) -> bool {
self.accept_task.is_some() && self.clear_task.is_some()
}
pub async fn list_sessions(&self) -> Vec<StorageToken> {
let sessions = self
.client_sessions
.iter()
.map(|item| item.value().clone())
.collect::<Vec<_>>();
let mut ret: Vec<StorageToken> = vec![];
for s in sessions {
if let Some(t) = s.get_token().await {
ret.push(t);
}
}
ret
}
pub fn get_session_by_machine_id(&self, machine_id: &uuid::Uuid) -> Option<Arc<Session>> {
let c_url = self.storage.get_client_url_by_machine_id(machine_id)?;
self.client_sessions
.get(&c_url)
.map(|item| item.value().clone())
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use easytier::{
tunnel::{
common::tests::wait_for_condition,
udp::{UdpTunnelConnector, UdpTunnelListener},
},
web_client::WebClient,
};
use crate::client_manager::ClientManager;
#[tokio::test]
async fn test_client() {
let listener = UdpTunnelListener::new("udp://0.0.0.0:54333".parse().unwrap());
let mut mgr = ClientManager::new();
mgr.serve(Box::new(listener)).await.unwrap();
let connector = UdpTunnelConnector::new("udp://127.0.0.1:54333".parse().unwrap());
let _c = WebClient::new(connector, "test");
wait_for_condition(
|| async { mgr.client_sessions.len() == 1 },
Duration::from_secs(6),
)
.await;
let mut a = mgr
.client_sessions
.iter()
.next()
.unwrap()
.data()
.read()
.await
.heartbeat_waiter();
let req = a.recv().await.unwrap();
println!("{:?}", req);
println!("{:?}", mgr);
}
}

View File

@ -0,0 +1,144 @@
use std::{fmt::Debug, sync::Arc};
use easytier::{
proto::{
rpc_impl::bidirect::BidirectRpcManager,
rpc_types::{self, controller::BaseController},
web::{
HeartbeatRequest, HeartbeatResponse, WebClientService, WebClientServiceClientFactory,
WebServerService, WebServerServiceServer,
},
},
tunnel::Tunnel,
};
use tokio::sync::{broadcast, RwLock};
use super::storage::{Storage, StorageToken, WeakRefStorage};
#[derive(Debug)]
pub struct SessionData {
storage: WeakRefStorage,
client_url: url::Url,
storage_token: Option<StorageToken>,
notifier: broadcast::Sender<HeartbeatRequest>,
req: Option<HeartbeatRequest>,
}
impl SessionData {
fn new(storage: WeakRefStorage, client_url: url::Url) -> Self {
let (tx, _rx1) = broadcast::channel(2);
SessionData {
storage,
client_url,
storage_token: None,
notifier: tx,
req: None,
}
}
pub fn req(&self) -> Option<HeartbeatRequest> {
self.req.clone()
}
pub fn heartbeat_waiter(&self) -> broadcast::Receiver<HeartbeatRequest> {
self.notifier.subscribe()
}
}
impl Drop for SessionData {
fn drop(&mut self) {
if let Ok(storage) = Storage::try_from(self.storage.clone()) {
if let Some(token) = self.storage_token.as_ref() {
storage.remove_client(token);
}
}
}
}
pub type SharedSessionData = Arc<RwLock<SessionData>>;
#[derive(Clone)]
struct SessionRpcService {
data: SharedSessionData,
}
#[async_trait::async_trait]
impl WebServerService for SessionRpcService {
type Controller = BaseController;
async fn heartbeat(
&self,
_: BaseController,
req: HeartbeatRequest,
) -> rpc_types::error::Result<HeartbeatResponse> {
let mut data = self.data.write().await;
if data.req.replace(req.clone()).is_none() {
assert!(data.storage_token.is_none());
data.storage_token = Some(StorageToken {
token: req.user_token.clone().into(),
client_url: data.client_url.clone(),
machine_id: req
.machine_id
.clone()
.map(Into::into)
.unwrap_or(uuid::Uuid::new_v4()),
});
if let Ok(storage) = Storage::try_from(data.storage.clone()) {
storage.add_client(data.storage_token.as_ref().unwrap().clone());
}
}
let _ = data.notifier.send(req);
Ok(HeartbeatResponse {})
}
}
pub struct Session {
rpc_mgr: BidirectRpcManager,
data: SharedSessionData,
}
impl Debug for Session {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Session").field("data", &self.data).finish()
}
}
impl Session {
pub fn new(tunnel: Box<dyn Tunnel>, storage: WeakRefStorage, client_url: url::Url) -> Self {
let rpc_mgr =
BidirectRpcManager::new().set_rx_timeout(Some(std::time::Duration::from_secs(30)));
rpc_mgr.run_with_tunnel(tunnel);
let data = Arc::new(RwLock::new(SessionData::new(storage, client_url)));
rpc_mgr.rpc_server().registry().register(
WebServerServiceServer::new(SessionRpcService { data: data.clone() }),
"",
);
Session { rpc_mgr, data }
}
pub fn is_running(&self) -> bool {
self.rpc_mgr.is_running()
}
pub fn data(&self) -> SharedSessionData {
self.data.clone()
}
pub fn scoped_rpc_client(
&self,
) -> Box<dyn WebClientService<Controller = BaseController> + Send> {
self.rpc_mgr
.rpc_client()
.scoped_client::<WebClientServiceClientFactory<BaseController>>(1, 1, "".to_string())
}
pub async fn get_token(&self) -> Option<StorageToken> {
self.data.read().await.storage_token.clone()
}
}

View File

@ -0,0 +1,72 @@
use std::sync::{Arc, Weak};
use dashmap::{DashMap, DashSet};
// use this to maintain Storage
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct StorageToken {
pub token: String,
pub client_url: url::Url,
pub machine_id: uuid::Uuid,
}
#[derive(Debug)]
pub struct StorageInner {
// some map for indexing
pub token_clients_map: DashMap<String, DashSet<url::Url>>,
pub machine_client_url_map: DashMap<uuid::Uuid, url::Url>,
}
#[derive(Debug, Clone)]
pub struct Storage(Arc<StorageInner>);
pub type WeakRefStorage = Weak<StorageInner>;
impl TryFrom<WeakRefStorage> for Storage {
type Error = ();
fn try_from(weak: Weak<StorageInner>) -> Result<Self, Self::Error> {
weak.upgrade().map(|inner| Storage(inner)).ok_or(())
}
}
impl Storage {
pub fn new() -> Self {
Storage(Arc::new(StorageInner {
token_clients_map: DashMap::new(),
machine_client_url_map: DashMap::new(),
}))
}
pub fn add_client(&self, stoken: StorageToken) {
let inner = self
.0
.token_clients_map
.entry(stoken.token)
.or_insert_with(DashSet::new);
inner.insert(stoken.client_url.clone());
self.0
.machine_client_url_map
.insert(stoken.machine_id, stoken.client_url.clone());
}
pub fn remove_client(&self, stoken: &StorageToken) {
self.0.token_clients_map.remove_if(&stoken.token, |_, set| {
set.remove(&stoken.client_url);
set.is_empty()
});
self.0.machine_client_url_map.remove(&stoken.machine_id);
}
pub fn weak_ref(&self) -> WeakRefStorage {
Arc::downgrade(&self.0)
}
pub fn get_client_url_by_machine_id(&self, machine_id: &uuid::Uuid) -> Option<url::Url> {
self.0
.machine_client_url_map
.get(&machine_id)
.map(|url| url.clone())
}
}

View File

@ -1,3 +1,22 @@
fn main() {
println!("Hello, world!");
#![allow(dead_code)]
use std::sync::Arc;
use easytier::tunnel::udp::UdpTunnelListener;
mod client_manager;
mod restful;
#[tokio::main]
async fn main() {
let listener = UdpTunnelListener::new("udp://0.0.0.0:22020".parse().unwrap());
let mut mgr = client_manager::ClientManager::new();
mgr.serve(listener).await.unwrap();
let mgr = Arc::new(mgr);
let mut restful_server =
restful::RestfulServer::new("0.0.0.0:11211".parse().unwrap(), mgr.clone());
restful_server.start().await.unwrap();
tokio::signal::ctrl_c().await.unwrap();
}

View File

@ -0,0 +1,246 @@
use std::vec;
use std::{net::SocketAddr, sync::Arc};
use axum::extract::{Path, Query};
use axum::http::StatusCode;
use axum::routing::post;
use axum::{extract::State, routing::get, Json, Router};
use easytier::proto::{self, rpc_types, web::*};
use easytier::{common::scoped_task::ScopedTask, proto::rpc_types::controller::BaseController};
use tokio::net::TcpListener;
use crate::client_manager::session::Session;
use crate::client_manager::storage::StorageToken;
use crate::client_manager::ClientManager;
pub struct RestfulServer {
bind_addr: SocketAddr,
client_mgr: Arc<ClientManager>,
serve_task: Option<ScopedTask<()>>,
}
type AppStateInner = Arc<ClientManager>;
type AppState = State<AppStateInner>;
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ListSessionJsonResp(Vec<StorageToken>);
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ValidateConfigJsonReq {
config: String,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct RunNetworkJsonReq {
config: String,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ColletNetworkInfoJsonReq {
inst_ids: Option<Vec<uuid::Uuid>>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct RemoveNetworkJsonReq {
inst_ids: Vec<uuid::Uuid>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ListNetworkInstanceIdsJsonResp(Vec<uuid::Uuid>);
type Error = proto::error::Error;
type ErrorKind = proto::error::error::ErrorKind;
type RpcError = rpc_types::error::Error;
type HttpHandleError = (StatusCode, Json<Error>);
fn convert_rpc_error(e: RpcError) -> (StatusCode, Json<Error>) {
let status_code = match &e {
RpcError::ExecutionError(_) => StatusCode::BAD_REQUEST,
RpcError::Timeout(_) => StatusCode::GATEWAY_TIMEOUT,
_ => StatusCode::BAD_GATEWAY,
};
let error = Error::from(&e);
(status_code, Json(error))
}
impl RestfulServer {
pub fn new(bind_addr: SocketAddr, client_mgr: Arc<ClientManager>) -> Self {
assert!(client_mgr.is_running());
RestfulServer {
bind_addr,
client_mgr,
serve_task: None,
}
}
async fn get_session_by_machine_id(
client_mgr: &ClientManager,
machine_id: &uuid::Uuid,
) -> Result<Arc<Session>, HttpHandleError> {
let Some(result) = client_mgr.get_session_by_machine_id(machine_id) else {
return Err((
StatusCode::NOT_FOUND,
Error {
error_kind: Some(ErrorKind::OtherError(proto::error::OtherError {
error_message: "No such session".to_string(),
})),
}
.into(),
));
};
Ok(result)
}
async fn handle_list_all_sessions(
State(client_mgr): AppState,
) -> Result<Json<ListSessionJsonResp>, HttpHandleError> {
let ret = client_mgr.list_sessions().await;
Ok(ListSessionJsonResp(ret).into())
}
async fn handle_validate_config(
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
Json(payload): Json<ValidateConfigJsonReq>,
) -> Result<(), HttpHandleError> {
let config = payload.config;
let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
c.validate_config(BaseController::default(), ValidateConfigRequest { config })
.await
.map_err(convert_rpc_error)?;
Ok(())
}
async fn handle_run_network_instance(
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
Json(payload): Json<RunNetworkJsonReq>,
) -> Result<(), HttpHandleError> {
let config = payload.config;
let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
c.run_network_instance(
BaseController::default(),
RunNetworkInstanceRequest { config },
)
.await
.map_err(convert_rpc_error)?;
Ok(())
}
async fn handle_collect_one_network_info(
State(client_mgr): AppState,
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
let ret = c
.collect_network_info(
BaseController::default(),
CollectNetworkInfoRequest {
inst_ids: vec![inst_id.into()],
},
)
.await
.map_err(convert_rpc_error)?;
Ok(ret.into())
}
async fn handle_collect_network_info(
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
Query(payload): Query<ColletNetworkInfoJsonReq>,
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
let ret = c
.collect_network_info(
BaseController::default(),
CollectNetworkInfoRequest {
inst_ids: payload
.inst_ids
.unwrap_or_default()
.into_iter()
.map(Into::into)
.collect(),
},
)
.await
.map_err(convert_rpc_error)?;
Ok(ret.into())
}
async fn handle_list_network_instance_ids(
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
) -> Result<Json<ListNetworkInstanceIdsJsonResp>, HttpHandleError> {
let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
let ret = c
.list_network_instance(BaseController::default(), ListNetworkInstanceRequest {})
.await
.map_err(convert_rpc_error)?;
Ok(
ListNetworkInstanceIdsJsonResp(ret.inst_ids.into_iter().map(Into::into).collect())
.into(),
)
}
async fn handle_remove_network_instance(
State(client_mgr): AppState,
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
) -> Result<(), HttpHandleError> {
let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
c.delete_network_instance(
BaseController::default(),
DeleteNetworkInstanceRequest {
inst_ids: vec![inst_id.into()],
},
)
.await
.map_err(convert_rpc_error)?;
Ok(())
}
pub async fn start(&mut self) -> Result<(), anyhow::Error> {
let listener = TcpListener::bind(self.bind_addr).await.unwrap();
let app = Router::new()
.route("/api/v1/sessions", get(Self::handle_list_all_sessions))
.route(
"/api/v1/network/:machine-id/validate-config",
post(Self::handle_validate_config),
)
.route(
"/api/v1/network/:machine-id",
post(Self::handle_run_network_instance).get(Self::handle_list_network_instance_ids),
)
.route(
"/api/v1/network/:machine-id/info",
get(Self::handle_collect_network_info),
)
.route(
"/api/v1/network/:machine-id/:inst-id",
get(Self::handle_collect_one_network_info)
.delete(Self::handle_remove_network_instance),
)
.with_state(self.client_mgr.clone());
let task = tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
self.serve_task = Some(task.into());
Ok(())
}
}

View File

@ -17,6 +17,7 @@ readme = "README.md"
[[bin]]
name = "easytier-core"
path = "src/easytier-core.rs"
test = false
[[bin]]
name = "easytier-cli"
@ -180,6 +181,9 @@ sys-locale = "0.3"
ringbuf = "0.4.5"
async-ringbuf = "0.3.1"
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
machine-uid = "0.5.3"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.52", features = [
"Win32_Networking_WinSock",

View File

@ -127,6 +127,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
"src/proto/error.proto",
"src/proto/tests.proto",
"src/proto/cli.proto",
"src/proto/web.proto",
];
for proto_file in &proto_files {
@ -138,6 +139,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.type_attribute(".common", "#[derive(serde::Serialize, serde::Deserialize)]")
.type_attribute(".error", "#[derive(serde::Serialize, serde::Deserialize)]")
.type_attribute(".cli", "#[derive(serde::Serialize, serde::Deserialize)]")
.type_attribute(".web", "#[derive(serde::Serialize, serde::Deserialize)]")
.type_attribute(
"peer_rpc.GetIpListResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",

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()));

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::*;

View File

@ -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(),

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?;

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();

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,

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;

View File

@ -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;
}

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;

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
}

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;

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()))
}

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;

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)
}
}

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();
}

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) {}
}

View File

@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/web.rs"));

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);

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(),
})
}
}

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;
}
}
}

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()
}
}