feat: Enable core to use local config files while being managed via the web (#1540)
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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">
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -280,6 +280,7 @@ impl Session {
|
||||
config: Some(
|
||||
serde_json::from_str::<NetworkConfig>(&c.network_config).unwrap(),
|
||||
),
|
||||
overwrite: false,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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 补全脚本"
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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! {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user