feat: Enable core to use local config files while being managed via the web (#1540)

This commit is contained in:
Mg Pig
2025-11-08 20:32:00 +08:00
committed by GitHub
parent b50744690e
commit 1273426009
24 changed files with 800 additions and 228 deletions

View File

@@ -2,7 +2,7 @@ use std::sync::Mutex;
use dashmap::DashMap;
use easytier::{
common::config::{ConfigLoader as _, TomlConfigLoader},
common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader},
instance_manager::NetworkInstanceManager,
};
@@ -128,7 +128,8 @@ pub unsafe extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char)
return -1;
}
let instance_id = match INSTANCE_MANAGER.run_network_instance(cfg, false) {
let instance_id =
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
Ok(id) => id,
Err(e) => {
set_error_msg(&format!("failed to start instance: {}", e));

View File

@@ -1,6 +1,6 @@
mod native_log;
use easytier::common::config::{ConfigLoader, TomlConfigLoader};
use easytier::common::config::{ConfigFileControl, ConfigLoader, TomlConfigLoader};
use easytier::common::constants::EASYTIER_VERSION;
use easytier::instance_manager::NetworkInstanceManager;
use napi_derive_ohos::napi;
@@ -75,7 +75,9 @@ pub fn run_network_instance(cfg_str: String) -> bool {
{
return false;
}
INSTANCE_MANAGER.run_network_instance(cfg, false).unwrap();
INSTANCE_MANAGER
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
.unwrap();
true
}

View File

@@ -8,7 +8,7 @@ use anyhow::Context as _;
use dashmap::DashMap;
use easytier::{
common::{
config::{ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader},
config::{ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader},
scoped_task::ScopedTask,
},
defer,
@@ -391,7 +391,7 @@ impl HealthChecker {
.delete_network_instance(vec![cfg.get_id()]);
});
self.instance_mgr
.run_network_instance(cfg.clone(), false)
.run_network_instance(cfg.clone(), false, ConfigFileControl::STATIC_CONFIG)
.with_context(|| "failed to run network instance")?;
let now = Instant::now();
@@ -435,7 +435,7 @@ impl HealthChecker {
);
self.instance_mgr
.run_network_instance(cfg.clone(), true)
.run_network_instance(cfg.clone(), true, ConfigFileControl::STATIC_CONFIG)
.with_context(|| "failed to run network instance")?;
self.inst_id_map.insert(node_id, cfg.get_id());

View File

@@ -78,7 +78,11 @@ fn generate_network_config(toml_config: String) -> Result<NetworkConfig, String>
}
#[tauri::command]
async fn run_network_instance(app: AppHandle, cfg: NetworkConfig) -> Result<(), String> {
async fn run_network_instance(
app: AppHandle,
cfg: NetworkConfig,
save: bool,
) -> Result<(), String> {
let instance_id = cfg.instance_id().to_string();
app.emit("pre_run_network_instance", cfg.instance_id())
@@ -97,7 +101,7 @@ async fn run_network_instance(app: AppHandle, cfg: NetworkConfig) -> Result<(),
CLIENT_MANAGER
.get()
.unwrap()
.handle_run_network_instance(app.clone(), cfg)
.handle_run_network_instance(app.clone(), cfg, save)
.await
.map_err(|e| e.to_string())?;
@@ -241,6 +245,7 @@ async fn get_config(app: AppHandle, instance_id: String) -> Result<NetworkConfig
#[tauri::command]
async fn load_configs(
app: AppHandle,
configs: Vec<NetworkConfig>,
enabled_networks: Vec<String>,
) -> Result<(), String> {
@@ -248,7 +253,7 @@ async fn load_configs(
.get()
.unwrap()
.storage
.load_configs(configs, enabled_networks)
.load_configs(app, configs, enabled_networks)
.await
.map_err(|e| e.to_string())?;
Ok(())
@@ -309,6 +314,7 @@ mod manager {
use async_trait::async_trait;
use dashmap::{DashMap, DashSet};
use easytier::launcher::NetworkConfig;
use easytier::proto::api::manage::RunNetworkInstanceRequest;
use easytier::proto::rpc_impl::bidirect::BidirectRpcManager;
use easytier::proto::rpc_types::controller::BaseController;
use easytier::rpc_service::remote_client::PersistentConfig;
@@ -340,6 +346,7 @@ mod manager {
pub(super) async fn load_configs(
&self,
app: AppHandle,
configs: Vec<NetworkConfig>,
enabled_networks: Vec<String>,
) -> anyhow::Result<()> {
@@ -353,11 +360,8 @@ mod manager {
}
self.enabled_networks.clear();
INSTANCE_MANAGER
.filter_network_instance(|_, _| true)
.into_iter()
.for_each(|id| {
self.enabled_networks.insert(id);
INSTANCE_MANAGER.iter().for_each(|v| {
self.enabled_networks.insert(*v.key());
});
for id in enabled_networks {
if let Ok(uuid) = id.parse() {
@@ -365,9 +369,22 @@ mod manager {
let config = self
.network_configs
.get(&uuid)
.map(|i| i.value().1.gen_config())
.ok_or_else(|| anyhow::anyhow!("Config not found"))??;
INSTANCE_MANAGER.run_network_instance(config, true)?;
.map(|i| i.value().1.clone())
.ok_or_else(|| anyhow::anyhow!("Config not found"))?;
CLIENT_MANAGER
.get()
.unwrap()
.get_rpc_client(app.clone())
.ok_or_else(|| anyhow::anyhow!("RPC client not found"))?
.run_network_instance(
BaseController::default(),
RunNetworkInstanceRequest {
inst_id: None,
config: Some(config),
overwrite: false,
},
)
.await?;
self.enabled_networks.insert(uuid);
}
}
@@ -438,18 +455,14 @@ mod manager {
app: AppHandle,
network_inst_id: Uuid,
disabled: bool,
) -> Result<GUIConfig, anyhow::Error> {
) -> Result<(), anyhow::Error> {
if disabled {
self.enabled_networks.remove(&network_inst_id);
} else {
self.enabled_networks.insert(network_inst_id);
}
self.save_enabled_networks(&app)?;
let cfg = self
.network_configs
.get(&network_inst_id)
.ok_or_else(|| anyhow::anyhow!("Config not found"))?;
Ok(cfg.value().clone())
Ok(())
}
async fn list_network_configs(

View File

@@ -15,8 +15,8 @@ export async function generateNetworkConfig(tomlConfig: string) {
return invoke<NetworkConfig>('generate_network_config', { tomlConfig })
}
export async function runNetworkInstance(cfg: NetworkConfig) {
return invoke('run_network_instance', { cfg })
export async function runNetworkInstance(cfg: NetworkConfig, save: boolean) {
return invoke('run_network_instance', { cfg, save })
}
export async function collectNetworkInfo(instanceId: string) {

View File

@@ -160,7 +160,7 @@ export async function onNetworkInstanceChange(instanceId: string) {
}
catch (e) {
console.error('start vpn service failed, stop all other network insts.', e)
await runNetworkInstance(config);
await runNetworkInstance(config, true); //on android config should always be saved
}
}
}

View File

@@ -5,8 +5,8 @@ export class GUIRemoteClient implements Api.RemoteClient {
async validate_config(config: NetworkTypes.NetworkConfig): Promise<Api.ValidateConfigResponse> {
return backend.validateConfig(config);
}
async run_network(config: NetworkTypes.NetworkConfig): Promise<undefined> {
await backend.runNetworkInstance(config);
async run_network(config: NetworkTypes.NetworkConfig, save: boolean): Promise<undefined> {
await backend.runNetworkInstance(config, save);
}
async get_network_info(inst_id: string): Promise<NetworkTypes.NetworkInstanceRunningInfo | undefined> {
return backend.collectNetworkInfo(inst_id).then(infos => infos.info.map[inst_id]);

View File

@@ -56,6 +56,23 @@ const onLazyLoadNetworkMetas = async (event: VirtualScrollerLazyEvent) => {
.map(item => item.uuid);
await loadNetworkMetas(instanceIds);
};
const currentNetworkMeta = computed(() => {
if (!instanceId.value) {
return undefined;
}
return networkMetaCache.value[instanceId.value];
});
const currentNetworkControl = {
remoteSave: computed(() => {
return Api.ConfigFilePermission.isRemoveSaveable(currentNetworkMeta.value?.config_permission ?? 0);
}),
editable: computed(() => {
return Api.ConfigFilePermission.isEditable(currentNetworkMeta.value?.config_permission ?? 0);
}),
deletable: computed(() => {
return Api.ConfigFilePermission.isDeletable(currentNetworkMeta.value?.config_permission ?? 0);
})
}
const instanceList = ref<Array<{ uuid: string; meta?: Api.NetworkMeta }>>([]);
const updateInstanceList = () => {
@@ -150,17 +167,12 @@ const loadCurrentNetworkConfig = async () => {
currentNetworkConfig.value = ret;
}
const updateNetworkState = async (disabled: boolean) => {
const stopNetwork = async () => {
if (!selectedInstanceId.value) {
return;
}
if (disabled || !currentNetworkConfig.value) {
await props.api.update_network_instance_state(selectedInstanceId.value.uuid, disabled);
} else if (currentNetworkConfig.value) {
await props.api.delete_network(currentNetworkConfig.value.instance_id);
await props.api.run_network(currentNetworkConfig.value);
}
await props.api.update_network_instance_state(selectedInstanceId.value.uuid, true);
await loadNetworkInstanceIds();
}
@@ -199,7 +211,7 @@ const saveAndRunNewNetwork = async () => {
}
try {
await props.api.delete_network(instanceId.value!);
let ret = await props.api.run_network(currentNetworkConfig.value);
let ret = await props.api.run_network(currentNetworkConfig.value, currentNetworkControl.remoteSave.value);
console.debug("saveAndRunNewNetwork", ret);
delete networkMetaCache.value[currentNetworkConfig.value.instance_id];
@@ -377,7 +389,7 @@ const actionMenu: Ref<MenuItem[]> = ref([
{
label: t('web.device_management.edit_network'),
icon: 'pi pi-pencil',
visible: () => !(networkIsDisabled.value ?? true),
visible: () => !(networkIsDisabled.value ?? true) && currentNetworkControl.editable.value,
command: () => editNetwork()
},
{
@@ -389,6 +401,7 @@ const actionMenu: Ref<MenuItem[]> = ref([
label: t('web.device_management.delete_network'),
icon: 'pi pi-trash',
class: 'p-error',
visible: () => currentNetworkControl.deletable.value,
command: () => confirmDeleteNetwork(new Event('click'))
}
]);
@@ -443,7 +456,7 @@ onUnmounted(() => {
<span class="truncate block">
&nbsp;
<span v-if="slotProps.value.meta">
{{ slotProps.value.meta.instance_name }} ({{ slotProps.value.uuid }})
{{ slotProps.value.meta.network_name }} ({{ slotProps.value.uuid }})
</span>
<span v-else>
{{ slotProps.value.uuid }}
@@ -463,7 +476,7 @@ onUnmounted(() => {
<div class="flex items-center min-w-0">
<div class="mr-4 min-w-0 flex-1">
<span class="truncate block">{{ t('network_name') }}: {{
slotProps.option.meta.instance_name }}</span>
slotProps.option.meta.network_name }}</span>
</div>
<Tag class="my-auto leading-3 shrink-0"
:severity="isRunning(slotProps.option.uuid) ? 'success' : 'info'"
@@ -544,8 +557,9 @@ onUnmounted(() => {
<Message v-else severity="error" class="mb-4">{{ curNetworkInfo?.error_msg }}</Message>
<div class="text-center mt-4">
<Button @click="updateNetworkState(true)" :label="t('web.device_management.disable_network')"
severity="warning" icon="pi pi-power-off" iconPos="left" />
<Button @click="stopNetwork" :disabled="!currentNetworkControl.deletable.value"
:label="t('web.device_management.disable_network')" severity="danger" icon="pi pi-power-off"
iconPos="left" />
</div>
</div>

View File

@@ -26,8 +26,27 @@ export interface CollectNetworkInfoResponse {
}
}
export namespace ConfigFilePermission {
export type Flags = number;
export const READ_ONLY: Flags = 1 << 0;
export const NO_DELETE: Flags = 1 << 1;
export function hasPermission(perm: Flags, flag: Flags): boolean {
return (perm & flag) === flag;
}
export function isRemoveSaveable(perm: Flags): boolean {
return !hasPermission(perm, NO_DELETE);
}
export function isEditable(perm: Flags): boolean {
return !hasPermission(perm, READ_ONLY);
}
export function isDeletable(perm: Flags): boolean {
return !hasPermission(perm, NO_DELETE);
}
}
export interface NetworkMeta {
instance_name: string;
network_name: string;
config_permission: ConfigFilePermission.Flags;
}
export interface GetNetworkMetasResponse {
@@ -36,7 +55,7 @@ export interface GetNetworkMetasResponse {
export interface RemoteClient {
validate_config(config: NetworkConfig): Promise<ValidateConfigResponse>;
run_network(config: NetworkConfig): Promise<undefined>;
run_network(config: NetworkConfig, save: boolean): Promise<undefined>;
get_network_info(inst_id: string): Promise<NetworkInstanceRunningInfo | undefined>;
list_network_instance_ids(): Promise<ListNetworkInstanceIdResponse>;
delete_network(inst_id: string): Promise<undefined>;

View File

@@ -193,9 +193,10 @@ class WebRemoteClient implements Api.RemoteClient {
});
return response;
}
async run_network(config: NetworkTypes.NetworkConfig): Promise<undefined> {
async run_network(config: NetworkTypes.NetworkConfig, save: boolean): Promise<undefined> {
await this.client.post<string>(`/machines/${this.machine_id}/networks`, {
config: config,
save: save
});
}
async get_network_info(inst_id: string): Promise<NetworkTypes.NetworkInstanceRunningInfo | undefined> {

View File

@@ -280,6 +280,7 @@ impl Session {
config: Some(
serde_json::from_str::<NetworkConfig>(&c.network_config).unwrap(),
),
overwrite: false,
},
)
.await;

View File

@@ -155,7 +155,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
(user_id, _): (UserIdInDb, Uuid),
network_inst_id: Uuid,
disabled: bool,
) -> Result<user_running_network_configs::Model, DbErr> {
) -> Result<(), DbErr> {
use entity::user_running_network_configs as urnc;
urnc::Entity::update_many()
@@ -169,15 +169,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
.exec(self.orm_db())
.await?;
urnc::Entity::find()
.filter(urnc::Column::UserId.eq(user_id))
.filter(urnc::Column::NetworkInstanceId.eq(network_inst_id.to_string()))
.one(self.orm_db())
.await?
.ok_or(DbErr::RecordNotFound(format!(
"Network config not found for user {} and network instance {}",
user_id, network_inst_id
)))
Ok(())
}
async fn list_network_configs(

View File

@@ -59,6 +59,7 @@ struct SaveNetworkJsonReq {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct RunNetworkJsonReq {
config: NetworkConfig,
save: bool,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
@@ -132,6 +133,7 @@ impl NetworkApi {
.handle_run_network_instance(
(Self::get_user_id(&auth_session)?, machine_id),
payload.config,
payload.save,
)
.await
.map_err(convert_error)?;

View File

@@ -18,6 +18,9 @@ core_clap:
config_file:
en: "path to the config file, NOTE: the options set by cmdline args will override options in config file"
zh-CN: "配置文件路径,注意:命令行中的配置的选项会覆盖配置文件中的选项"
config_dir:
en: "Load all .toml files in the directory to start network instances, and store the received configurations in this directory."
zh-CN: "加载目录中的所有 .toml 文件以启动网络实例,并将下发的配置保存在此目录中。"
generate_completions:
en: "generate shell completions"
zh-CN: "生成 shell 补全脚本"

View File

@@ -7,6 +7,7 @@ use std::{
use anyhow::Context;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncReadExt as _;
use crate::{
common::stun::StunInfoCollector,
@@ -829,6 +830,157 @@ impl ConfigLoader for TomlConfigLoader {
}
}
#[derive(Clone, Copy, Default)]
pub struct ConfigFilePermission(u8);
impl ConfigFilePermission {
pub const READ_ONLY: u8 = 1 << 0;
pub const NO_DELETE: u8 = 1 << 1;
pub fn with_flag(self, flag: u8) -> Self {
Self(self.0 | flag)
}
pub fn remove_flag(self, flag: u8) -> Self {
Self(self.0 & !flag)
}
pub fn has_flag(&self, flag: u8) -> bool {
(self.0 & flag) != 0
}
}
impl From<u8> for ConfigFilePermission {
fn from(value: u8) -> Self {
ConfigFilePermission(value)
}
}
impl From<u32> for ConfigFilePermission {
fn from(value: u32) -> Self {
ConfigFilePermission(value as u8)
}
}
impl From<ConfigFilePermission> for u8 {
fn from(value: ConfigFilePermission) -> Self {
value.0
}
}
impl From<ConfigFilePermission> for u32 {
fn from(value: ConfigFilePermission) -> Self {
value.0 as u32
}
}
impl std::fmt::Debug for ConfigFilePermission {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut flags = vec![];
if self.has_flag(ConfigFilePermission::READ_ONLY) {
flags.push("READ_ONLY");
} else {
flags.push("EDITABLE");
}
if self.has_flag(ConfigFilePermission::NO_DELETE) {
flags.push("NO_DELETE");
} else {
flags.push("DELETABLE");
}
write!(f, "{}", flags.join("|"))
}
}
#[derive(Debug, Clone)]
pub struct ConfigFileControl {
pub path: Option<PathBuf>,
pub permission: ConfigFilePermission,
}
impl ConfigFileControl {
pub const STATIC_CONFIG: ConfigFileControl = Self {
path: None,
permission: ConfigFilePermission(
ConfigFilePermission::READ_ONLY | ConfigFilePermission::NO_DELETE,
),
};
pub fn new(path: Option<PathBuf>, permission: ConfigFilePermission) -> Self {
ConfigFileControl { path, permission }
}
pub async fn from_path(path: PathBuf) -> Self {
let read_only = if let Ok(metadata) = tokio::fs::metadata(&path).await {
metadata.permissions().readonly()
} else {
true
};
Self::new(
Some(path),
if read_only {
ConfigFilePermission(ConfigFilePermission::READ_ONLY)
} else {
ConfigFilePermission(0)
},
)
}
pub fn is_read_only(&self) -> bool {
self.permission.has_flag(ConfigFilePermission::READ_ONLY)
}
pub fn set_read_only(&mut self, read_only: bool) {
if read_only {
self.permission = self.permission.with_flag(ConfigFilePermission::READ_ONLY);
} else {
self.permission = self.permission.remove_flag(ConfigFilePermission::READ_ONLY);
}
}
pub fn is_no_delete(&self) -> bool {
self.permission.has_flag(ConfigFilePermission::NO_DELETE)
}
pub fn set_no_delete(&mut self, no_delete: bool) {
if no_delete {
self.permission = self.permission.with_flag(ConfigFilePermission::NO_DELETE);
} else {
self.permission = self.permission.remove_flag(ConfigFilePermission::NO_DELETE);
}
}
pub fn is_deletable(&self) -> bool {
!self.is_no_delete()
}
}
pub async fn load_config_from_file(
config_file: &PathBuf,
config_dir: Option<&PathBuf>,
) -> Result<(TomlConfigLoader, ConfigFileControl), anyhow::Error> {
if config_file.as_os_str() == "-" {
let mut stdin = String::new();
_ = tokio::io::stdin()
.read_to_string(&mut stdin)
.await
.context("failed to read config from stdin")?;
let config = TomlConfigLoader::new_from_str(&stdin)?;
return Ok((config, ConfigFileControl::STATIC_CONFIG));
}
let config = TomlConfigLoader::new(config_file)
.with_context(|| format!("failed to load config file: {:?}", config_file))?;
let mut control = ConfigFileControl::from_path(config_file.clone()).await;
if control.is_read_only() {
control.set_no_delete(true);
} else if let Some(config_dir) = config_dir {
if let Some(config_file_dir) = config_file.parent() {
// if the config file is in the config dir and named as the instance id, it can be saved remotely
if config_file_dir == config_dir
&& config_file.file_stem() == Some(config.get_id().to_string().as_ref())
&& config_file.extension() == Some(std::ffi::OsStr::new("toml"))
{
control.set_no_delete(false);
} else {
control.set_no_delete(true);
}
}
} else {
control.set_no_delete(true);
}
Ok((config, control))
}
#[cfg(test)]
pub mod tests {
use super::*;

View File

@@ -17,22 +17,18 @@ use clap_complete::Shell;
use easytier::{
common::{
config::{
get_avaliable_encrypt_methods, ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig,
LoggingConfigLoader, NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader,
VpnPortalConfig,
get_avaliable_encrypt_methods, load_config_from_file, ConfigFileControl, ConfigLoader,
ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader, NetworkIdentity,
PeerConfig, PortForwardConfig, TomlConfigLoader, VpnPortalConfig,
},
constants::EASYTIER_VERSION,
global_ctx::GlobalCtx,
set_default_machine_id,
stun::MockStunInfoCollector,
},
connector::create_connector_by_url,
defer,
instance_manager::NetworkInstanceManager,
launcher::add_proxy_network_to_config,
proto::common::{CompressionAlgoPb, NatType},
proto::common::CompressionAlgoPb,
rpc_service::ApiRpcServer,
tunnel::{IpVersion, PROTO_PORT_OFFSET},
tunnel::PROTO_PORT_OFFSET,
utils::{init_logger, setup_panic_handler},
web_client,
};
@@ -136,6 +132,13 @@ struct Cli {
)]
config_file: Option<Vec<PathBuf>>,
#[arg(
long,
env = "ET_CONFIG_DIR",
help = t!("core_clap.config_dir").to_string()
)]
config_dir: Option<PathBuf>,
#[command(flatten)]
network_options: NetworkOptions,
@@ -152,7 +155,7 @@ struct Cli {
check_config: bool,
}
#[derive(Parser, Debug)]
#[derive(Parser, Debug, Default, PartialEq, Eq)]
struct NetworkOptions {
#[arg(
long,
@@ -707,6 +710,9 @@ impl Cli {
impl NetworkOptions {
fn can_merge(&self, cfg: &TomlConfigLoader, config_file_count: usize) -> bool {
if (*self) == NetworkOptions::default() {
return false;
}
if config_file_count == 1 {
return true;
}
@@ -1141,7 +1147,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
defer!(dump_profile(0););
init_logger(&cli.logging_options, true)?;
let manager = Arc::new(NetworkInstanceManager::new());
let manager = Arc::new(NetworkInstanceManager::new().with_config_path(cli.config_dir.clone()));
let _rpc_server = ApiRpcServer::new(
cli.rpc_portal_options.rpc_portal,
@@ -1151,93 +1157,77 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
.serve()
.await?;
if cli.config_server.is_some() {
set_default_machine_id(cli.machine_id);
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.cn:22020/{}",
config_server_url_s
let _web_client = if let Some(config_server_url_s) = cli.config_server.as_ref() {
let wc = web_client::run_web_client(
config_server_url_s,
cli.machine_id.clone(),
cli.network_options.hostname.clone(),
manager.clone(),
)
.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| percent_encoding::percent_decode_str(x).decode_utf8())
.transpose()
.with_context(|| "failed to decode config server token")?
.map(|x| x.to_string())
.unwrap_or_default();
.await
.inspect(|_| {
println!(
"Entering config client mode...\n server: {}\n token: {}",
c_url, token,
"Web client started successfully...\nserver: {}",
config_server_url_s,
);
println!("Official config website: https://easytier.cn/web");
})?;
if token.is_empty() {
panic!("empty token");
}
let config = TomlConfigLoader::default();
let global_ctx = Arc::new(GlobalCtx::new(config));
global_ctx.replace_stun_info_collector(Box::new(MockStunInfoCollector {
udp_nat_type: NatType::Unknown,
}));
let mut flags = global_ctx.get_flags();
flags.bind_device = false;
global_ctx.set_flags(flags);
let hostname = match cli.network_options.hostname {
None => gethostname::gethostname().to_string_lossy().to_string(),
Some(hostname) => hostname.to_string(),
};
let _wc = web_client::WebClient::new(
create_connector_by_url(c_url.as_str(), &global_ctx, IpVersion::Both).await?,
token.to_string(),
hostname,
manager,
);
tokio::signal::ctrl_c().await.unwrap();
return Ok(());
}
let mut crate_cli_network =
cli.config_file.is_none() || cli.network_options.network_name.is_some();
if let Some(config_files) = cli.config_file {
let config_file_count = config_files.len();
for config_file in config_files {
let mut cfg = if config_file.as_os_str() == "-" {
let mut stdin = String::new();
_ = tokio::io::stdin().read_to_string(&mut stdin).await?;
TomlConfigLoader::new_from_str(stdin.as_str())
.with_context(|| "failed to load config from stdin")?
Some(wc)
} else {
TomlConfigLoader::new(&config_file)
.with_context(|| format!("failed to load config file: {:?}", config_file))?
None
};
let mut config_files = if let Some(v) = cli.config_file {
v.clone()
} else {
vec![]
};
if let Some(config_dir) = cli.config_dir.as_ref() {
if !config_dir.is_dir() {
anyhow::bail!("config_dir {} is not a directory", config_dir.display());
}
for entry in std::fs::read_dir(config_dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
let Some(ext) = path.extension() else {
continue;
};
if ext != "toml" {
continue;
}
config_files.push(path);
}
}
let config_file_count = config_files.len();
let mut crate_cli_network = (config_file_count == 0 && cli.config_server.is_none())
|| cli.network_options.network_name.is_some();
for config_file in config_files {
let (mut cfg, mut control) =
load_config_from_file(&config_file, cli.config_dir.as_ref()).await?;
if cli.network_options.can_merge(&cfg, config_file_count) {
cli.network_options.merge_into(&mut cfg).with_context(|| {
format!("failed to merge config from cli: {:?}", config_file)
})?;
cli.network_options
.merge_into(&mut cfg)
.with_context(|| format!("failed to merge config from cli: {:?}", config_file))?;
crate_cli_network = false;
control.set_read_only(true);
control.set_no_delete(true);
}
println!(
"Starting easytier from config file {:?} with config:",
config_file
"Starting easytier from config file {:?}({:?}) with config:",
config_file, control.permission
);
println!("############### TOML ###############\n");
println!("{}", cfg.dump());
println!("-----------------------------------");
manager.run_network_instance(cfg, true)?;
}
manager.run_network_instance(cfg, true, control)?;
}
if crate_cli_network {
@@ -1249,7 +1239,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
println!("############### TOML ###############\n");
println!("{}", cfg.dump());
println!("-----------------------------------");
manager.run_network_instance(cfg, true)?;
manager.run_network_instance(cfg, true, ConfigFileControl::STATIC_CONFIG)?;
}
tokio::select! {

View File

@@ -1,10 +1,10 @@
use std::{collections::BTreeMap, sync::Arc};
use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
use dashmap::DashMap;
use crate::{
common::{
config::{ConfigLoader, TomlConfigLoader},
config::{ConfigFileControl, ConfigLoader, TomlConfigLoader},
global_ctx::{EventBusSubscriber, GlobalCtxEvent},
scoped_task::ScopedTask,
},
@@ -13,11 +13,24 @@ use crate::{
rpc_service::InstanceRpcService,
};
pub(crate) struct WebClientGuard {
guard: Option<Arc<()>>,
stop_check_notifier: Arc<tokio::sync::Notify>,
}
impl Drop for WebClientGuard {
fn drop(&mut self) {
drop(self.guard.take());
self.stop_check_notifier.notify_one();
}
}
pub struct NetworkInstanceManager {
instance_map: Arc<DashMap<uuid::Uuid, NetworkInstance>>,
instance_stop_tasks: Arc<DashMap<uuid::Uuid, ScopedTask<()>>>,
stop_check_notifier: Arc<tokio::sync::Notify>,
instance_error_messages: Arc<DashMap<uuid::Uuid, String>>,
config_dir: Option<PathBuf>,
web_client_counter: Arc<()>,
}
impl Default for NetworkInstanceManager {
@@ -33,9 +46,16 @@ impl NetworkInstanceManager {
instance_stop_tasks: Arc::new(DashMap::new()),
stop_check_notifier: Arc::new(tokio::sync::Notify::new()),
instance_error_messages: Arc::new(DashMap::new()),
config_dir: None,
web_client_counter: Arc::new(()),
}
}
pub fn with_config_path(mut self, config_dir: Option<PathBuf>) -> Self {
self.config_dir = config_dir;
self
}
fn start_instance_task(&self, instance_id: uuid::Uuid) -> Result<(), anyhow::Error> {
if tokio::runtime::Handle::try_current().is_err() {
return Err(anyhow::anyhow!(
@@ -83,13 +103,14 @@ impl NetworkInstanceManager {
&self,
cfg: TomlConfigLoader,
watch_event: bool,
config_file_control: ConfigFileControl,
) -> Result<uuid::Uuid, 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);
let mut instance = NetworkInstance::new(cfg, config_file_control);
instance.start()?;
self.instance_map.insert(instance_id, instance);
@@ -174,18 +195,20 @@ impl NetworkInstanceManager {
pub fn get_network_instance_name(&self, instance_id: &uuid::Uuid) -> Option<String> {
self.instance_map
.get(instance_id)
.map(|instance| instance.value().get_inst_name())
.map(|instance| instance.value().get_network_name())
}
pub fn filter_network_instance(
pub fn iter(&self) -> dashmap::iter::Iter<'_, uuid::Uuid, NetworkInstance> {
self.instance_map.iter()
}
pub fn get_instance_config_control(
&self,
filter: impl Fn(&uuid::Uuid, &NetworkInstance) -> bool,
) -> Vec<uuid::Uuid> {
instance_id: &uuid::Uuid,
) -> Option<ConfigFileControl> {
self.instance_map
.iter()
.filter(|item| filter(item.key(), item.value()))
.map(|item| *item.key())
.collect()
.get(instance_id)
.map(|instance| instance.value().get_config_file_control().clone())
}
pub fn get_instance_service(
@@ -206,12 +229,33 @@ impl NetworkInstanceManager {
Ok(())
}
pub fn get_config_dir(&self) -> Option<&PathBuf> {
self.config_dir.as_ref()
}
pub(crate) fn register_web_client(&self) -> WebClientGuard {
WebClientGuard {
guard: Some(self.web_client_counter.clone()),
stop_check_notifier: self.stop_check_notifier.clone(),
}
}
pub(crate) fn notify_stop_check(&self) {
self.stop_check_notifier.notify_one();
}
pub async fn wait(&self) {
while self
loop {
let local_instance_running = self
.instance_map
.iter()
.any(|item| item.value().is_easytier_running())
{
.any(|item| item.value().is_easytier_running());
let web_client_running = Arc::strong_count(&self.web_client_counter) > 1;
if !local_instance_running && !web_client_running {
break;
}
self.stop_check_notifier.notified().await;
}
}
@@ -417,19 +461,36 @@ mod tests {
})
.unwrap(),
true,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
let instance_id2 = manager
.run_network_instance(TomlConfigLoader::new_from_str(cfg_str).unwrap(), true)
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
true,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
let instance_id3 = manager
.run_network_instance(TomlConfigLoader::new_from_str(cfg_str).unwrap(), false)
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
false,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
let instance_id4 = manager
.run_network_instance(TomlConfigLoader::new_from_str(cfg_str).unwrap(), true)
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
true,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
let instance_id5 = manager
.run_network_instance(TomlConfigLoader::new_from_str(cfg_str).unwrap(), false)
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
false,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
tokio::time::sleep(std::time::Duration::from_secs(1)).await; // to make instance actually started
@@ -464,10 +525,18 @@ mod tests {
let port = crate::utils::find_free_tcp_port(10012..65534).expect("no free tcp port found");
assert!(manager
.run_network_instance(TomlConfigLoader::new_from_str(cfg_str).unwrap(), true,)
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
true,
ConfigFileControl::STATIC_CONFIG
)
.is_err());
assert!(manager
.run_network_instance(TomlConfigLoader::new_from_str(cfg_str).unwrap(), true,)
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
true,
ConfigFileControl::STATIC_CONFIG
)
.is_err());
assert!(manager
.run_network_instance(
@@ -477,13 +546,22 @@ mod tests {
})
.unwrap(),
false,
ConfigFileControl::STATIC_CONFIG
)
.is_ok());
assert!(manager
.run_network_instance(TomlConfigLoader::new_from_str(cfg_str).unwrap(), true,)
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
true,
ConfigFileControl::STATIC_CONFIG
)
.is_err());
assert!(manager
.run_network_instance(TomlConfigLoader::new_from_str(cfg_str).unwrap(), false,)
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
false,
ConfigFileControl::STATIC_CONFIG
)
.is_ok());
std::thread::sleep(std::time::Duration::from_secs(1)); // wait instance actually started
@@ -526,6 +604,7 @@ mod tests {
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str.as_str()).unwrap(),
watch_event,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
@@ -557,6 +636,7 @@ mod tests {
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str.as_str()).unwrap(),
watch_event,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
@@ -584,6 +664,7 @@ mod tests {
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str.as_str()).unwrap(),
true,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
@@ -593,6 +674,7 @@ mod tests {
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str.as_str()).unwrap(),
true,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();

View File

@@ -1,4 +1,4 @@
use crate::common::config::PortForwardConfig;
use crate::common::config::{ConfigFileControl, PortForwardConfig};
use crate::proto::api::{self, manage};
use crate::proto::rpc_types::controller::BaseController;
use crate::rpc_service::InstanceRpcService;
@@ -284,13 +284,15 @@ pub type NetworkInstanceRunningInfo = crate::proto::api::manage::NetworkInstance
pub struct NetworkInstance {
config: TomlConfigLoader,
launcher: Option<EasyTierLauncher>,
config_file_control: ConfigFileControl,
}
impl NetworkInstance {
pub fn new(config: TomlConfigLoader) -> Self {
pub fn new(config: TomlConfigLoader, config_file_control: ConfigFileControl) -> Self {
Self {
config,
launcher: None,
config_file_control,
}
}
@@ -387,6 +389,10 @@ impl NetworkInstance {
self.config.get_inst_name()
}
pub fn get_network_name(&self) -> String {
self.config.get_network_identity().network_name
}
pub fn set_tun_fd(&mut self, tun_fd: i32) {
if let Some(launcher) = self.launcher.as_ref() {
launcher.data.tun_fd.write().unwrap().replace(tun_fd);
@@ -422,6 +428,10 @@ impl NetworkInstance {
.map(|launcher| launcher.data.instance_stop_notifier.clone())
}
pub fn get_config_file_control(&self) -> &ConfigFileControl {
&self.config_file_control
}
pub fn get_latest_error_msg(&self) -> Option<String> {
if let Some(launcher) = self.launcher.as_ref() {
launcher.error_msg.read().unwrap().clone()

View File

@@ -112,6 +112,12 @@ message NetworkInstanceRunningInfoMap {
map<string, NetworkInstanceRunningInfo> map = 1;
}
message NetworkMeta {
common.UUID inst_id = 1;
string network_name = 2;
uint32 config_permission = 3;
}
message ValidateConfigRequest { NetworkConfig config = 1; }
message ValidateConfigResponse { string toml_config = 1; }
@@ -119,6 +125,7 @@ message ValidateConfigResponse { string toml_config = 1; }
message RunNetworkInstanceRequest {
common.UUID inst_id = 1;
NetworkConfig config = 2;
bool overwrite = 3;
}
message RunNetworkInstanceResponse { common.UUID inst_id = 1; }
@@ -143,6 +150,14 @@ message DeleteNetworkInstanceResponse {
repeated common.UUID remain_inst_ids = 1;
}
message GetNetworkInstanceConfigRequest { common.UUID inst_id = 1; }
message GetNetworkInstanceConfigResponse { NetworkConfig config = 1; }
message ListNetworkInstanceMetaRequest { repeated common.UUID inst_ids = 1; }
message ListNetworkInstanceMetaResponse { repeated NetworkMeta metas = 1; }
service WebClientService {
rpc ValidateConfig(ValidateConfigRequest) returns (ValidateConfigResponse) {}
rpc RunNetworkInstance(RunNetworkInstanceRequest)
@@ -155,4 +170,8 @@ service WebClientService {
returns (ListNetworkInstanceResponse) {}
rpc DeleteNetworkInstance(DeleteNetworkInstanceRequest)
returns (DeleteNetworkInstanceResponse) {}
rpc GetNetworkInstanceConfig(GetNetworkInstanceConfigRequest)
returns (GetNetworkInstanceConfigResponse) {}
rpc ListNetworkInstanceMeta(ListNetworkInstanceMetaRequest)
returns (ListNetworkInstanceMetaResponse) {}
}

View File

@@ -1,10 +1,10 @@
use std::sync::Arc;
use std::{collections::HashSet, sync::Arc};
use crate::{
common::config::ConfigLoader,
common::config::{ConfigFileControl, ConfigFilePermission, ConfigLoader},
instance_manager::NetworkInstanceManager,
proto::{
api::manage::*,
api::{config::GetConfigRequest, manage::*},
rpc_types::{self, controller::BaseController},
},
};
@@ -46,11 +46,59 @@ impl WebClientService for InstanceManageRpcService {
if let Some(inst_id) = req.inst_id {
cfg.set_id(inst_id.into());
}
self.manager.run_network_instance(cfg, true)?;
println!("instance {} started", id);
Ok(RunNetworkInstanceResponse {
let resp = RunNetworkInstanceResponse {
inst_id: Some(id.into()),
})
};
let mut control = if let Some(control) = self.manager.get_instance_config_control(&id) {
if !req.overwrite {
return Ok(resp);
}
if control.is_read_only() {
return Err(
anyhow::anyhow!("instance {} is read-only, cannot be overwritten", id).into(),
);
}
if let Some(path) = control.path.as_ref() {
let real_control = ConfigFileControl::from_path(path.clone()).await;
if real_control.is_read_only() {
return Err(anyhow::anyhow!(
"config file {} is read-only, cannot be overwritten",
path.display()
)
.into());
}
}
self.manager.delete_network_instance(vec![id])?;
control.clone()
} else if let Some(config_dir) = self.manager.get_config_dir() {
ConfigFileControl::new(
Some(config_dir.join(format!("{}.toml", id))),
ConfigFilePermission::default(),
)
} else {
ConfigFileControl::new(None, ConfigFilePermission::default())
};
if !control.is_read_only() {
if let Some(config_file) = control.path.as_ref() {
if let Err(e) = std::fs::write(config_file, cfg.dump()) {
tracing::warn!(
"failed to write config file {}: {}",
config_file.display(),
e
);
control.set_read_only(true);
}
}
}
self.manager.run_network_instance(cfg, true, control)?;
println!("instance {} started", id);
Ok(resp)
}
async fn retain_network_instance(
@@ -124,12 +172,79 @@ impl WebClientService for InstanceManageRpcService {
_: BaseController,
req: DeleteNetworkInstanceRequest,
) -> Result<DeleteNetworkInstanceResponse, rpc_types::error::Error> {
let remain_inst_ids = self
let inst_ids: HashSet<uuid::Uuid> = req.inst_ids.into_iter().map(Into::into).collect();
let inst_ids = self
.manager
.delete_network_instance(req.inst_ids.into_iter().map(Into::into).collect())?;
.iter()
.filter(|v| inst_ids.contains(v.key()))
.filter(|v| v.get_config_file_control().is_deletable())
.map(|v| *v.key())
.collect::<Vec<_>>();
let config_files = inst_ids
.iter()
.filter_map(|id| {
self.manager
.get_instance_config_control(id)
.and_then(|control| control.path.clone())
})
.collect::<Vec<_>>();
let remain_inst_ids = self.manager.delete_network_instance(inst_ids)?;
println!("instance {:?} retained", remain_inst_ids);
for config_file in config_files {
if let Err(e) = std::fs::remove_file(&config_file) {
tracing::warn!(
"failed to remove config file {}: {}",
config_file.display(),
e
);
}
}
Ok(DeleteNetworkInstanceResponse {
remain_inst_ids: remain_inst_ids.into_iter().map(Into::into).collect(),
})
}
async fn get_network_instance_config(
&self,
_: BaseController,
req: GetNetworkInstanceConfigRequest,
) -> Result<GetNetworkInstanceConfigResponse, rpc_types::error::Error> {
let inst_id: uuid::Uuid = req
.inst_id
.ok_or_else(|| anyhow::anyhow!("instance id is required"))?
.into();
let config = self
.manager
.get_instance_service(&inst_id)
.ok_or_else(|| anyhow::anyhow!("instance service not found"))?
.get_config_service()
.get_config(BaseController::default(), GetConfigRequest::default())
.await?
.config;
Ok(GetNetworkInstanceConfigResponse { config })
}
async fn list_network_instance_meta(
&self,
_: BaseController,
req: ListNetworkInstanceMetaRequest,
) -> Result<ListNetworkInstanceMetaResponse, rpc_types::error::Error> {
let mut metas = Vec::with_capacity(req.inst_ids.len());
for inst_id in req.inst_ids {
let inst_id: uuid::Uuid = (inst_id).into();
let Some(control) = self.manager.get_instance_config_control(&inst_id) else {
continue;
};
let Some(name) = self.manager.get_network_instance_name(&inst_id) else {
continue;
};
let meta = NetworkMeta {
inst_id: Some(inst_id.into()),
network_name: name,
config_permission: control.permission.into(),
};
metas.push(meta);
}
Ok(ListNetworkInstanceMetaResponse { metas })
}
}

View File

@@ -79,18 +79,23 @@ fn get_instance_service(
let id = if let Some(api::instance::instance_identifier::Selector::Id(id)) = selector {
(*id).into()
} else {
let ids = instance_manager.filter_network_instance(|_, i| {
if let Some(api::instance::instance_identifier::Selector::InstanceSelector(selector)) =
selector
let ids = instance_manager
.iter()
.filter(|v| {
if let Some(api::instance::instance_identifier::Selector::InstanceSelector(
selector,
)) = selector
{
if let Some(name) = selector.name.as_ref() {
if i.get_inst_name() != *name {
if v.get_inst_name() != *name {
return false;
}
}
}
true
});
})
.map(|v| *v.key())
.collect::<Vec<_>>();
match ids.len() {
0 => return Err(anyhow::anyhow!("No instance matches the selector")),
1 => ids[0],

View File

@@ -40,6 +40,7 @@ where
&self,
identify: T,
config: NetworkConfig,
save: bool,
) -> Result<(), RemoteClientError<E>> {
let client = self
.get_rpc_client(identify.clone())
@@ -50,10 +51,12 @@ where
RunNetworkInstanceRequest {
inst_id: None,
config: Some(config.clone()),
overwrite: true,
},
)
.await?;
if save {
self.get_storage()
.insert_or_update_user_network_config(
identify,
@@ -62,6 +65,7 @@ where
)
.await
.map_err(RemoteClientError::PersistentError)?;
}
Ok(())
}
@@ -156,13 +160,17 @@ where
let client = self
.get_rpc_client(identify.clone())
.ok_or(RemoteClientError::ClientNotFound)?;
let cfg = self
.get_storage()
.update_network_config_state(identify, inst_id, disabled)
.handle_get_network_config(identify.clone(), inst_id)
.await?;
if disabled {
self.get_storage()
.insert_or_update_user_network_config(identify.clone(), inst_id, cfg.clone())
.await
.map_err(RemoteClientError::PersistentError)?;
if disabled {
client
.delete_network_instance(
BaseController::default(),
@@ -177,15 +185,18 @@ where
BaseController::default(),
RunNetworkInstanceRequest {
inst_id: Some(inst_id.into()),
config: Some(
cfg.get_network_config()
.map_err(RemoteClientError::PersistentError)?,
),
config: Some(cfg),
overwrite: true,
},
)
.await?;
}
self.get_storage()
.update_network_config_state(identify, inst_id, disabled)
.await
.map_err(RemoteClientError::PersistentError)?;
Ok(())
}
@@ -196,14 +207,38 @@ where
) -> Result<GetNetworkMetasResponse, RemoteClientError<E>> {
let mut metas = std::collections::HashMap::new();
if let Some(client) = self.get_rpc_client(identify.clone()) {
if let Ok(resp) = client
.list_network_instance_meta(
BaseController::default(),
ListNetworkInstanceMetaRequest {
inst_ids: inst_ids.iter().cloned().map(|id| id.into()).collect(),
},
)
.await
{
for meta in resp.metas {
if let Some(inst_id) = meta.inst_id.as_ref() {
let inst_id: uuid::Uuid = (*inst_id).into();
metas.insert(inst_id, meta);
}
}
}
}
for instance_id in inst_ids {
if metas.contains_key(&instance_id) {
continue;
}
let config = self
.handle_get_network_config(identify.clone(), instance_id)
.await?;
metas.insert(
instance_id,
NetworkMeta {
instance_name: config.network_name.unwrap_or_default(),
inst_id: Some(instance_id.into()),
network_name: config.network_name.unwrap_or_default(),
config_permission: 0,
},
);
}
@@ -233,6 +268,22 @@ where
identify: T,
inst_id: uuid::Uuid,
) -> Result<NetworkConfig, RemoteClientError<E>> {
if let Some(client) = self.get_rpc_client(identify.clone()) {
if let Ok(resp) = client
.get_network_instance_config(
BaseController::default(),
GetNetworkInstanceConfigRequest {
inst_id: Some(inst_id.into()),
},
)
.await
{
if let Some(config) = resp.config {
return Ok(config);
}
}
}
let inst_id = inst_id.to_string();
let db_row = self
@@ -277,11 +328,6 @@ pub struct ListNetworkInstanceIdsJsonResp {
disabled_inst_ids: Vec<crate::proto::common::Uuid>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct NetworkMeta {
instance_name: String,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct GetNetworkMetasResponse {
metas: std::collections::HashMap<uuid::Uuid, NetworkMeta>,
@@ -312,7 +358,7 @@ where
identify: T,
network_inst_id: Uuid,
disabled: bool,
) -> Result<C, E>;
) -> Result<(), E>;
async fn list_network_configs(&self, identify: T, props: ListNetworkProps)
-> Result<Vec<C>, E>;

View File

@@ -35,4 +35,8 @@ impl Controller {
pub fn get_rpc_service(&self) -> InstanceManageRpcService {
InstanceManageRpcService::new(self.manager.clone())
}
pub(super) fn notify_manager_stopping(&self) {
self.manager.notify_stop_check();
}
}

View File

@@ -1,9 +1,17 @@
use std::sync::Arc;
use crate::{
common::scoped_task::ScopedTask, instance_manager::NetworkInstanceManager,
tunnel::TunnelConnector,
common::{
config::TomlConfigLoader, global_ctx::GlobalCtx, scoped_task::ScopedTask,
set_default_machine_id, stun::MockStunInfoCollector,
},
connector::create_connector_by_url,
instance_manager::{NetworkInstanceManager, WebClientGuard},
proto::common::NatType,
tunnel::{IpVersion, TunnelConnector},
};
use anyhow::{Context as _, Result};
use url::Url;
pub mod controller;
pub mod session;
@@ -11,6 +19,7 @@ pub mod session;
pub struct WebClient {
controller: Arc<controller::Controller>,
tasks: ScopedTask<()>,
manager_guard: WebClientGuard,
}
impl WebClient {
@@ -20,6 +29,7 @@ impl WebClient {
hostname: H,
manager: Arc<NetworkInstanceManager>,
) -> Self {
let manager_guard = manager.register_web_client();
let controller = Arc::new(controller::Controller::new(
token.to_string(),
hostname.to_string(),
@@ -31,7 +41,11 @@ impl WebClient {
Self::routine(controller_clone, Box::new(connector)).await;
}));
WebClient { controller, tasks }
WebClient {
controller,
tasks,
manager_guard,
}
}
async fn routine(
@@ -58,3 +72,90 @@ impl WebClient {
}
}
}
pub async fn run_web_client(
config_server_url_s: &str,
machine_id: Option<String>,
hostname: Option<String>,
manager: Arc<NetworkInstanceManager>,
) -> Result<WebClient> {
set_default_machine_id(machine_id);
let config_server_url = match Url::parse(config_server_url_s) {
Ok(u) => u,
Err(_) => format!(
"udp://config-server.easytier.cn: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| percent_encoding::percent_decode_str(x).decode_utf8())
.transpose()
.with_context(|| "failed to decode config server token")?
.map(|x| x.to_string())
.unwrap_or_default();
if token.is_empty() {
return Err(anyhow::anyhow!("empty token"));
}
let config = TomlConfigLoader::default();
let global_ctx = Arc::new(GlobalCtx::new(config));
global_ctx.replace_stun_info_collector(Box::new(MockStunInfoCollector {
udp_nat_type: NatType::Unknown,
}));
let mut flags = global_ctx.get_flags();
flags.bind_device = false;
global_ctx.set_flags(flags);
let hostname = match hostname {
None => gethostname::gethostname().to_string_lossy().to_string(),
Some(hostname) => hostname,
};
Ok(WebClient::new(
create_connector_by_url(c_url.as_str(), &global_ctx, IpVersion::Both).await?,
token.to_string(),
hostname,
manager.clone(),
))
}
#[cfg(test)]
mod tests {
use std::sync::{atomic::AtomicBool, Arc};
use crate::instance_manager::NetworkInstanceManager;
#[tokio::test]
async fn test_manager_wait() {
let manager = Arc::new(NetworkInstanceManager::new());
let client = super::run_web_client(
format!("ring://{}/test", uuid::Uuid::new_v4()).as_str(),
None,
None,
manager.clone(),
)
.await
.unwrap();
let sleep_finish = Arc::new(AtomicBool::new(false));
let sleep_finish_clone = sleep_finish.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
println!("Dropping client...");
sleep_finish_clone.store(true, std::sync::atomic::Ordering::Relaxed);
drop(client);
println!("Client dropped.");
});
println!("Waiting for manager...");
manager.wait().await;
assert!(sleep_finish.load(std::sync::atomic::Ordering::Relaxed));
println!("Manager stopped.");
}
}