diff --git a/easytier-contrib/easytier-ffi/src/lib.rs b/easytier-contrib/easytier-ffi/src/lib.rs index 098e5f3..c4dc9b8 100644 --- a/easytier-contrib/easytier-ffi/src/lib.rs +++ b/easytier-contrib/easytier-ffi/src/lib.rs @@ -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,13 +128,14 @@ 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) { - Ok(id) => id, - Err(e) => { - set_error_msg(&format!("failed to start instance: {}", e)); - return -1; - } - }; + 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)); + return -1; + } + }; INSTANCE_NAME_ID_MAP.insert(inst_name, instance_id); diff --git a/easytier-contrib/easytier-ohrs/src/lib.rs b/easytier-contrib/easytier-ohrs/src/lib.rs index e6a1386..a535c88 100644 --- a/easytier-contrib/easytier-ohrs/src/lib.rs +++ b/easytier-contrib/easytier-ohrs/src/lib.rs @@ -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 } diff --git a/easytier-contrib/easytier-uptime/src/health_checker.rs b/easytier-contrib/easytier-uptime/src/health_checker.rs index ba6741c..2d83e29 100644 --- a/easytier-contrib/easytier-uptime/src/health_checker.rs +++ b/easytier-contrib/easytier-uptime/src/health_checker.rs @@ -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()); diff --git a/easytier-gui/src-tauri/src/lib.rs b/easytier-gui/src-tauri/src/lib.rs index 7ac4346..6570c61 100644 --- a/easytier-gui/src-tauri/src/lib.rs +++ b/easytier-gui/src-tauri/src/lib.rs @@ -78,7 +78,11 @@ fn generate_network_config(toml_config: String) -> Result } #[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, enabled_networks: Vec, ) -> 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, enabled_networks: Vec, ) -> anyhow::Result<()> { @@ -353,21 +360,31 @@ 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() { if !self.enabled_networks.contains(&uuid) { 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 { + ) -> 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( diff --git a/easytier-gui/src/composables/backend.ts b/easytier-gui/src/composables/backend.ts index c1c5da6..5491ace 100644 --- a/easytier-gui/src/composables/backend.ts +++ b/easytier-gui/src/composables/backend.ts @@ -15,8 +15,8 @@ export async function generateNetworkConfig(tomlConfig: string) { return invoke('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) { diff --git a/easytier-gui/src/composables/mobile_vpn.ts b/easytier-gui/src/composables/mobile_vpn.ts index 8ef0744..49791e6 100644 --- a/easytier-gui/src/composables/mobile_vpn.ts +++ b/easytier-gui/src/composables/mobile_vpn.ts @@ -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 } } } diff --git a/easytier-gui/src/modules/api.ts b/easytier-gui/src/modules/api.ts index e212d4b..627206e 100644 --- a/easytier-gui/src/modules/api.ts +++ b/easytier-gui/src/modules/api.ts @@ -5,8 +5,8 @@ export class GUIRemoteClient implements Api.RemoteClient { async validate_config(config: NetworkTypes.NetworkConfig): Promise { return backend.validateConfig(config); } - async run_network(config: NetworkTypes.NetworkConfig): Promise { - await backend.runNetworkInstance(config); + async run_network(config: NetworkTypes.NetworkConfig, save: boolean): Promise { + await backend.runNetworkInstance(config, save); } async get_network_info(inst_id: string): Promise { return backend.collectNetworkInfo(inst_id).then(infos => infos.info.map[inst_id]); diff --git a/easytier-web/frontend-lib/src/components/RemoteManagement.vue b/easytier-web/frontend-lib/src/components/RemoteManagement.vue index c94503a..a6ad4e2 100644 --- a/easytier-web/frontend-lib/src/components/RemoteManagement.vue +++ b/easytier-web/frontend-lib/src/components/RemoteManagement.vue @@ -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>([]); 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 = 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 = 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(() => {   - {{ slotProps.value.meta.instance_name }} ({{ slotProps.value.uuid }}) + {{ slotProps.value.meta.network_name }} ({{ slotProps.value.uuid }}) {{ slotProps.value.uuid }} @@ -463,7 +476,7 @@ onUnmounted(() => {
{{ t('network_name') }}: {{ - slotProps.option.meta.instance_name }} + slotProps.option.meta.network_name }}
{ {{ curNetworkInfo?.error_msg }}
-
diff --git a/easytier-web/frontend-lib/src/modules/api.ts b/easytier-web/frontend-lib/src/modules/api.ts index eb64e64..e85a741 100644 --- a/easytier-web/frontend-lib/src/modules/api.ts +++ b/easytier-web/frontend-lib/src/modules/api.ts @@ -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; - run_network(config: NetworkConfig): Promise; + run_network(config: NetworkConfig, save: boolean): Promise; get_network_info(inst_id: string): Promise; list_network_instance_ids(): Promise; delete_network(inst_id: string): Promise; diff --git a/easytier-web/frontend/src/modules/api.ts b/easytier-web/frontend/src/modules/api.ts index f70c552..8ce8ca2 100644 --- a/easytier-web/frontend/src/modules/api.ts +++ b/easytier-web/frontend/src/modules/api.ts @@ -193,9 +193,10 @@ class WebRemoteClient implements Api.RemoteClient { }); return response; } - async run_network(config: NetworkTypes.NetworkConfig): Promise { + async run_network(config: NetworkTypes.NetworkConfig, save: boolean): Promise { await this.client.post(`/machines/${this.machine_id}/networks`, { config: config, + save: save }); } async get_network_info(inst_id: string): Promise { diff --git a/easytier-web/src/client_manager/session.rs b/easytier-web/src/client_manager/session.rs index e585223..d892be6 100644 --- a/easytier-web/src/client_manager/session.rs +++ b/easytier-web/src/client_manager/session.rs @@ -280,6 +280,7 @@ impl Session { config: Some( serde_json::from_str::(&c.network_config).unwrap(), ), + overwrite: false, }, ) .await; diff --git a/easytier-web/src/db/mod.rs b/easytier-web/src/db/mod.rs index 6f66044..c2b4e98 100644 --- a/easytier-web/src/db/mod.rs +++ b/easytier-web/src/db/mod.rs @@ -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 { + ) -> 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( diff --git a/easytier-web/src/restful/network.rs b/easytier-web/src/restful/network.rs index be67e93..7dc59fb 100644 --- a/easytier-web/src/restful/network.rs +++ b/easytier-web/src/restful/network.rs @@ -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)?; diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml index 0c23824..3dff1cd 100644 --- a/easytier/locales/app.yml +++ b/easytier/locales/app.yml @@ -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 补全脚本" diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index c9f4745..5eb5b59 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -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 for ConfigFilePermission { + fn from(value: u8) -> Self { + ConfigFilePermission(value) + } +} +impl From for ConfigFilePermission { + fn from(value: u32) -> Self { + ConfigFilePermission(value as u8) + } +} +impl From for u8 { + fn from(value: ConfigFilePermission) -> Self { + value.0 + } +} +impl From 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, + 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, 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::*; diff --git a/easytier/src/easytier-core.rs b/easytier/src/easytier-core.rs index 6448d37..dac9960 100644 --- a/easytier/src/easytier-core.rs +++ b/easytier/src/easytier-core.rs @@ -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>, + #[arg( + long, + env = "ET_CONFIG_DIR", + help = t!("core_clap.config_dir").to_string() + )] + config_dir: Option, + #[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 - ) - .parse() - .unwrap(), - }; + 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(), + ) + .await + .inspect(|_| { + println!( + "Web client started successfully...\nserver: {}", + config_server_url_s, + ); - 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(); + println!("Official config website: https://easytier.cn/web"); + })?; + + Some(wc) + } else { + 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))?; + crate_cli_network = false; + control.set_read_only(true); + control.set_no_delete(true); + } println!( - "Entering config client mode...\n server: {}\n token: {}", - c_url, token, + "Starting easytier from config file {:?}({:?}) with config:", + config_file, control.permission ); - - 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")? - } else { - TomlConfigLoader::new(&config_file) - .with_context(|| format!("failed to load config file: {:?}", config_file))? - }; - - 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) - })?; - crate_cli_network = false; - } - - println!( - "Starting easytier from config file {:?} with config:", - config_file - ); - println!("############### TOML ###############\n"); - println!("{}", cfg.dump()); - println!("-----------------------------------"); - manager.run_network_instance(cfg, true)?; - } + println!("############### TOML ###############\n"); + println!("{}", cfg.dump()); + println!("-----------------------------------"); + 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! { diff --git a/easytier/src/instance_manager.rs b/easytier/src/instance_manager.rs index 3ce9ad7..1e3976e 100644 --- a/easytier/src/instance_manager.rs +++ b/easytier/src/instance_manager.rs @@ -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>, + stop_check_notifier: Arc, +} +impl Drop for WebClientGuard { + fn drop(&mut self) { + drop(self.guard.take()); + self.stop_check_notifier.notify_one(); + } +} + pub struct NetworkInstanceManager { instance_map: Arc>, instance_stop_tasks: Arc>>, stop_check_notifier: Arc, instance_error_messages: Arc>, + config_dir: Option, + 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) -> 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 { 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 { 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 { + instance_id: &uuid::Uuid, + ) -> Option { 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 - .instance_map - .iter() - .any(|item| item.value().is_easytier_running()) - { + loop { + let local_instance_running = self + .instance_map + .iter() + .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(); diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index 52330a4..f5c9088 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -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, + 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 { if let Some(launcher) = self.launcher.as_ref() { launcher.error_msg.read().unwrap().clone() diff --git a/easytier/src/proto/api_manage.proto b/easytier/src/proto/api_manage.proto index 923a9e8..736f14d 100644 --- a/easytier/src/proto/api_manage.proto +++ b/easytier/src/proto/api_manage.proto @@ -112,6 +112,12 @@ message NetworkInstanceRunningInfoMap { map 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) {} } diff --git a/easytier/src/rpc_service/instance_manage.rs b/easytier/src/rpc_service/instance_manage.rs index 61ab21f..7a15143 100644 --- a/easytier/src/rpc_service/instance_manage.rs +++ b/easytier/src/rpc_service/instance_manage.rs @@ -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 { - let remain_inst_ids = self + let inst_ids: HashSet = 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::>(); + let config_files = inst_ids + .iter() + .filter_map(|id| { + self.manager + .get_instance_config_control(id) + .and_then(|control| control.path.clone()) + }) + .collect::>(); + 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 { + 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 { + 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 }) + } } diff --git a/easytier/src/rpc_service/mod.rs b/easytier/src/rpc_service/mod.rs index 90cad8d..e67c2fb 100644 --- a/easytier/src/rpc_service/mod.rs +++ b/easytier/src/rpc_service/mod.rs @@ -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 - { - if let Some(name) = selector.name.as_ref() { - if i.get_inst_name() != *name { - return false; + 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 v.get_inst_name() != *name { + return false; + } } } - } - true - }); + true + }) + .map(|v| *v.key()) + .collect::>(); match ids.len() { 0 => return Err(anyhow::anyhow!("No instance matches the selector")), 1 => ids[0], diff --git a/easytier/src/rpc_service/remote_client.rs b/easytier/src/rpc_service/remote_client.rs index 1d28000..511c5ba 100644 --- a/easytier/src/rpc_service/remote_client.rs +++ b/easytier/src/rpc_service/remote_client.rs @@ -40,6 +40,7 @@ where &self, identify: T, config: NetworkConfig, + save: bool, ) -> Result<(), RemoteClientError> { let client = self .get_rpc_client(identify.clone()) @@ -50,18 +51,21 @@ where RunNetworkInstanceRequest { inst_id: None, config: Some(config.clone()), + overwrite: true, }, ) .await?; - self.get_storage() - .insert_or_update_user_network_config( - identify, - resp.inst_id.unwrap_or_default().into(), - config, - ) - .await - .map_err(RemoteClientError::PersistentError)?; + if save { + self.get_storage() + .insert_or_update_user_network_config( + identify, + resp.inst_id.unwrap_or_default().into(), + config, + ) + .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) - .await - .map_err(RemoteClientError::PersistentError)?; + .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)?; + 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> { 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> { + 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, } -#[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, @@ -312,7 +358,7 @@ where identify: T, network_inst_id: Uuid, disabled: bool, - ) -> Result; + ) -> Result<(), E>; async fn list_network_configs(&self, identify: T, props: ListNetworkProps) -> Result, E>; diff --git a/easytier/src/web_client/controller.rs b/easytier/src/web_client/controller.rs index d681e13..354c9c8 100644 --- a/easytier/src/web_client/controller.rs +++ b/easytier/src/web_client/controller.rs @@ -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(); + } } diff --git a/easytier/src/web_client/mod.rs b/easytier/src/web_client/mod.rs index 14748ab..b5a5fb8 100644 --- a/easytier/src/web_client/mod.rs +++ b/easytier/src/web_client/mod.rs @@ -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, tasks: ScopedTask<()>, + manager_guard: WebClientGuard, } impl WebClient { @@ -20,6 +29,7 @@ impl WebClient { hostname: H, manager: Arc, ) -> 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, + hostname: Option, + manager: Arc, +) -> Result { + 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."); + } +}