Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990)
Some checks failed
EasyTier Core / pre_job (push) Has been cancelled
EasyTier GUI / pre_job (push) Has been cancelled
EasyTier Mobile / pre_job (push) Has been cancelled
EasyTier Test / pre_job (push) Has been cancelled
EasyTier Core / build_web (push) Has been cancelled
EasyTier Core / build (freebsd-13.2-x86_64, 13.2, ubuntu-22.04, x86_64-unknown-freebsd) (push) Has been cancelled
EasyTier Core / build (linux-aarch64, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-arm, ubuntu-22.04, arm-unknown-linux-musleabi) (push) Has been cancelled
EasyTier Core / build (linux-armhf, ubuntu-22.04, arm-unknown-linux-musleabihf) (push) Has been cancelled
EasyTier Core / build (linux-armv7, ubuntu-22.04, armv7-unknown-linux-musleabi) (push) Has been cancelled
EasyTier Core / build (linux-armv7hf, ubuntu-22.04, armv7-unknown-linux-musleabihf) (push) Has been cancelled
EasyTier Core / build (linux-mips, ubuntu-22.04, mips-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-mipsel, ubuntu-22.04, mipsel-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-x86_64, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (macos-aarch64, macos-latest, aarch64-apple-darwin) (push) Has been cancelled
EasyTier Core / build (macos-x86_64, macos-latest, x86_64-apple-darwin) (push) Has been cancelled
EasyTier Core / build (windows-arm64, windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / build (windows-i686, windows-latest, i686-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / build (windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / core-result (push) Has been cancelled
EasyTier Core / magisk_build (push) Has been cancelled
EasyTier GUI / build-gui (linux-aarch64, aarch64-unknown-linux-gnu, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Has been cancelled
EasyTier GUI / build-gui (linux-x86_64, x86_64-unknown-linux-gnu, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Has been cancelled
EasyTier GUI / build-gui (macos-aarch64, aarch64-apple-darwin, macos-latest, aarch64-apple-darwin) (push) Has been cancelled
EasyTier GUI / build-gui (macos-x86_64, x86_64-apple-darwin, macos-latest, x86_64-apple-darwin) (push) Has been cancelled
EasyTier GUI / build-gui (windows-arm64, aarch64-pc-windows-msvc, windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / build-gui (windows-i686, i686-pc-windows-msvc, windows-latest, i686-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / build-gui (windows-x86_64, x86_64-pc-windows-msvc, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / gui-result (push) Has been cancelled
EasyTier Mobile / build-mobile (android, ubuntu-22.04, android) (push) Has been cancelled
EasyTier Mobile / mobile-result (push) Has been cancelled
EasyTier Test / test (push) Has been cancelled

* add method to create NetworkConfig from TomlConfigLoader
* allow web export/import toml config file and gui edit toml config
* Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web
This commit is contained in:
Mg Pig
2025-06-15 23:41:42 +08:00
committed by GitHub
parent 40b5fe9a54
commit ed162c2e66
18 changed files with 738 additions and 80 deletions

View File

@@ -50,7 +50,11 @@ dev_name_placeholder: 注意当多个网络同时使用相同的TUN接口名
off_text: 点击关闭
on_text: 点击开启
show_config: 显示配置
edit_config: 编辑配置文件
close: 关闭
save: 保存
config_saved: 配置已保存
use_latency_first: 延迟优先模式
my_node_info: 当前节点信息

View File

@@ -51,7 +51,10 @@ dev_name_placeholder: 'Note: When multiple networks use the same TUN interface n
off_text: Press to disable
on_text: Press to enable
show_config: Show Config
edit_config: Edit Config File
close: Close
save: Save
config_saved: Configuration saved
my_node_info: My Node Info
peer_count: Connected
upload: Upload

View File

@@ -4,11 +4,9 @@
use std::collections::BTreeMap;
use easytier::{
common::config::{
ConfigLoader, FileLoggerConfig, LoggingConfigBuilder,
},
launcher::{ConfigSource, NetworkConfig, NetworkInstanceRunningInfo},
common::config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader},
instance_manager::NetworkInstanceManager,
launcher::{ConfigSource, NetworkConfig, NetworkInstanceRunningInfo},
utils::{self, NewFilterSender},
};
@@ -44,6 +42,13 @@ fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
Ok(toml.dump())
}
#[tauri::command]
fn generate_network_config(toml_config: String) -> Result<NetworkConfig, String> {
let config = TomlConfigLoader::new_from_str(&toml_config).map_err(|e| e.to_string())?;
let cfg = NetworkConfig::new_from_config(&config).map_err(|e| e.to_string())?;
Ok(cfg)
}
#[tauri::command]
fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> {
let instance_id = cfg.instance_id().to_string();
@@ -226,6 +231,7 @@ pub fn run() {
})
.invoke_handler(tauri::generate_handler![
parse_network_config,
generate_network_config,
run_network_instance,
retain_network_instance,
collect_network_infos,

View File

@@ -8,5 +8,6 @@ onBeforeMount(async () => {
</script>
<template>
<Toast position="bottom-right" />
<RouterView />
</template>

View File

@@ -23,6 +23,7 @@ declare global {
const effectScope: typeof import('vue')['effectScope']
const event2human: typeof import('./composables/utils')['event2human']
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
const generateNetworkConfig: typeof import('./composables/network')['generateNetworkConfig']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
@@ -134,6 +135,7 @@ declare module 'vue' {
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']>
readonly generateNetworkConfig: UnwrapRef<typeof import('./composables/network')['generateNetworkConfig']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>

View File

@@ -8,6 +8,10 @@ export async function parseNetworkConfig(cfg: NetworkConfig) {
return invoke<string>('parse_network_config', { cfg })
}
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 })
}

View File

@@ -8,7 +8,7 @@ import { exit } from '@tauri-apps/plugin-process'
import { open } from '@tauri-apps/plugin-shell'
import TieredMenu from 'primevue/tieredmenu'
import { useToast } from 'primevue/usetoast'
import { NetworkTypes, Config, Status, Utils, I18nUtils } from 'easytier-frontend-lib'
import { NetworkTypes, Config, Status, Utils, I18nUtils, ConfigEditDialog } from 'easytier-frontend-lib'
import { isAutostart, setLoggingLevel } from '~/composables/network'
import { useTray } from '~/composables/tray'
@@ -23,7 +23,7 @@ useTray(true)
const items = ref([
{
label: () => t('show_config'),
label: () => activeStep.value == "2" ? t('show_config') : t('edit_config'),
icon: 'pi pi-file-edit',
command: async () => {
try {
@@ -262,6 +262,13 @@ onMounted(async () => {
function isRunning(id: string) {
return networkStore.networkInstanceIds.includes(id)
}
async function saveTomlConfig(tomlConfig: string) {
const config = await generateNetworkConfig(tomlConfig)
networkStore.replaceCurNetwork(config);
toast.add({ severity: 'success', detail: t('config_saved'), life: 3000 })
visible.value = false
}
</script>
<script lang="ts">
@@ -269,17 +276,8 @@ function isRunning(id: string) {
<template>
<div id="root" class="flex flex-col">
<Dialog v-model:visible="visible" modal header="Config File" :style="{ width: '70%' }">
<Panel>
<ScrollPanel style="width: 100%; height: 300px">
<pre>{{ tomlConfig }}</pre>
</ScrollPanel>
</Panel>
<Divider />
<div class="flex gap-2 justify-end">
<Button type="button" :label="t('close')" @click="visible = false" />
</div>
</Dialog>
<ConfigEditDialog v-model:visible="visible" :cur-network="curNetworkConfig" :readonly="activeStep !== '1'"
:save-config="saveTomlConfig" :generate-config="parseNetworkConfig" />
<Dialog v-model:visible="aboutVisible" modal :header="t('about.title')" :style="{ width: '70%' }">
<About />

View File

@@ -48,6 +48,12 @@ export const useNetworkStore = defineStore('networkStore', {
this.curNetwork = this.networkList[nextCurNetworkIdx]
},
replaceCurNetwork(cfg: NetworkTypes.NetworkConfig) {
const curNetworkIdx = this.networkList.indexOf(this.curNetwork)
this.networkList[curNetworkIdx] = cfg
this.curNetwork = cfg
},
removeNetworkInstance(instanceId: string) {
delete this.instances[instanceId]
},

View File

@@ -0,0 +1,103 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { NetworkConfig } from '../types/network';
import { Divider, Button, Dialog, Textarea } from 'primevue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
readonly: {
type: Boolean,
default: false,
},
generateConfig: {
type: Object as () => (config: NetworkConfig) => Promise<string>,
required: true,
},
saveConfig: {
type: Object as () => (config: string) => Promise<void>,
required: true,
},
})
const curNetwork = defineModel('curNetwork', {
type: Object as () => NetworkConfig | undefined,
required: true,
})
const visible = defineModel('visible', {
type: Boolean,
default: false,
})
watch([visible, curNetwork], async ([newVisible, newCurNetwork]) => {
if (!newVisible) {
tomlConfig.value = '';
return;
}
if (!newCurNetwork) {
tomlConfig.value = '';
return;
}
const config = newCurNetwork;
try {
errorMessage.value = '';
tomlConfig.value = await props.generateConfig(config);
} catch (e) {
errorMessage.value = 'Failed to generate config: ' + (e instanceof Error ? e.message : String(e));
tomlConfig.value = '';
}
})
onMounted(async () => {
if (!visible.value) {
return;
}
if (!curNetwork.value) {
tomlConfig.value = '';
return;
}
const config = curNetwork.value;
try {
tomlConfig.value = await props.generateConfig(config);
errorMessage.value = '';
} catch (e) {
errorMessage.value = 'Failed to generate config: ' + (e instanceof Error ? e.message : String(e));
tomlConfig.value = '';
}
});
const handleConfigSave = async () => {
if (props.readonly) return;
try {
await props.saveConfig(tomlConfig.value);
visible.value = false;
} catch (e) {
errorMessage.value = 'Failed to save config: ' + (e instanceof Error ? e.message : String(e));
}
};
const tomlConfig = ref<string>('')
const tomlConfigRows = ref<number>(1);
const errorMessage = ref<string>('');
watch(tomlConfig, (newValue) => {
tomlConfigRows.value = newValue.split('\n').length;
errorMessage.value = '';
});
</script>
<template>
<Dialog v-model:visible="visible" modal :header="t('config_file')" :style="{ width: '70%' }">
<pre v-if="errorMessage"
class="mb-2 p-2 rounded text-sm overflow-auto bg-red-100 text-red-700 max-h-40">{{ errorMessage }}</pre>
<div class="flex w-full" style="max-height: 60vh; overflow-y: auto;">
<Textarea v-model="tomlConfig" class="w-full h-full font-mono flex flex-col resize-none" :rows="tomlConfigRows"
spellcheck="false" :readonly="props.readonly"></Textarea>
</div>
<Divider />
<div class="flex gap-2 justify-end">
<Button v-if="!props.readonly" type="button" :label="t('save')" @click="handleConfigSave" />
<Button type="button" :label="t('close')" @click="visible = false" />
</div>
</Dialog>
</template>

View File

@@ -1,2 +1,3 @@
export { default as Config } from './Config.vue';
export { default as Status } from './Status.vue';
export { default as ConfigEditDialog } from './ConfigEditDialog.vue';

View File

@@ -1,7 +1,7 @@
import './style.css'
import type { App } from 'vue';
import { Config, Status } from "./components";
import { Config, Status, ConfigEditDialog } from "./components";
import Aura from '@primevue/themes/aura'
import PrimeVue from 'primevue/config'
@@ -41,10 +41,11 @@ export default {
});
app.component('Config', Config);
app.component('ConfigEditDialog', ConfigEditDialog);
app.component('Status', Status);
app.component('HumanEvent', HumanEvent);
app.directive('tooltip', vTooltip as any);
}
};
export { Config, Status, I18nUtils, NetworkTypes, Api, Utils };
export { Config, ConfigEditDialog, Status, I18nUtils, NetworkTypes, Api, Utils };

View File

@@ -51,7 +51,11 @@ dev_name_placeholder: 注意当多个网络同时使用相同的TUN接口名
off_text: 点击关闭
on_text: 点击开启
show_config: 显示配置
edit_config: 编辑配置文件
config_file: 配置文件
close: 关闭
save: 保存
config_saved: 配置已保存
use_latency_first: 延迟优先模式
my_node_info: 当前节点信息

View File

@@ -52,7 +52,11 @@ dev_name_placeholder: 'Note: When multiple networks use the same TUN interface n
off_text: Press to disable
on_text: Press to enable
show_config: Show Config
edit_config: Edit Config File
config_file: Config File
close: Close
save: Save
config_saved: Configuration saved
my_node_info: My Node Info
peer_count: Connected
upload: Upload

View File

@@ -47,6 +47,15 @@ export interface GenerateConfigResponse {
error?: string;
}
export interface ParseConfigRequest {
toml_config: string;
}
export interface ParseConfigResponse {
config?: NetworkConfig;
error?: string;
}
export class ApiClient {
private client: AxiosInstance;
private authFailedCb: Function | undefined;
@@ -215,6 +224,18 @@ export class ApiClient {
return { error: 'Unknown error: ' + error };
}
}
public async parse_config(config: ParseConfigRequest): Promise<ParseConfigResponse> {
try {
const response = await this.client.post<any, ParseConfigResponse>('/parse-config', config);
return response;
} catch (error) {
if (error instanceof AxiosError) {
return { error: error.response?.data };
}
return { error: 'Unknown error: ' + error };
}
}
}
export default ApiClient;

View File

@@ -2,12 +2,11 @@
import { NetworkTypes } from 'easytier-frontend-lib';
import {computed, ref} from 'vue';
import { Api } from 'easytier-frontend-lib'
import {AutoComplete, Divider} from "primevue";
import {AutoComplete, Divider, Button, Textarea} from "primevue";
import {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} from "../modules/api-host"
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
const apiHost = ref<string>(getInitialApiHost())
const apiHostSuggestions = ref<Array<string>>([])
const apiHostSearch = async (event: { query: string }) => {
@@ -22,23 +21,46 @@ const apiHostSearch = async (event: { query: string }) => {
}
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
const toml_config = ref<string>("Press 'Run Network' to generate TOML configuration");
const toml_config = ref<string>("");
const errorMessage = ref<string>("");
const generateConfig = (config: NetworkTypes.NetworkConfig) => {
saveApiHost(apiHost.value)
errorMessage.value = "";
api.value?.generate_config({
config: config
}).then((res) => {
if (res.error) {
toml_config.value = res.error;
errorMessage.value = "Generation failed: " + res.error;
} else if (res.toml_config) {
toml_config.value = res.toml_config;
} else {
toml_config.value = "Api server returned an unexpected response";
errorMessage.value = "Api server returned an unexpected response";
}
}).catch(err => {
errorMessage.value = "Generate request failed: " + (err instanceof Error ? err.message : String(err));
});
};
const parseConfig = async () => {
try {
errorMessage.value = "";
const res = await api.value?.parse_config({
toml_config: toml_config.value
});
if (res.error) {
errorMessage.value = "Parse failed: " + res.error;
} else if (res.config) {
newNetworkConfig.value = res.config;
} else {
errorMessage.value = "API returned an unexpected response";
}
} catch (e) {
errorMessage.value = "Parse request failed: " + (e instanceof Error ? e.message : String(e));
}
};
</script>
<template>
@@ -55,8 +77,17 @@ const generateConfig = (config: NetworkTypes.NetworkConfig) => {
</div>
<Config :cur-network="newNetworkConfig" @run-network="generateConfig" />
</div>
<div class="sm:w-full md:w-1/2 p-4 bg-gray-100">
<pre class="whitespace-pre-wrap">{{ toml_config }}</pre>
<div class="sm:w-full md:w-1/2 p-4 flex flex-col h-[calc(100vh-80px)]">
<pre v-if="errorMessage" class="mb-2 p-2 rounded text-sm overflow-auto bg-red-100 text-red-700 max-h-40">{{ errorMessage }}</pre>
<Textarea
v-model="toml_config"
spellcheck="false"
class="w-full flex-grow p-2 bg-gray-100 whitespace-pre-wrap font-mono border-none focus:outline-none resize-none"
placeholder="Press 'Run Network' to generate TOML configuration, or paste your TOML configuration here to parse it"
></Textarea>
<div class="mt-3 flex justify-center">
<Button label="Parse Config" icon="pi pi-arrow-left" icon-pos="left" @click="parseConfig" />
</div>
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { Toolbar, IftaLabel, Select, Button, ConfirmPopup, Dialog, useConfirm, useToast, Divider } from 'primevue';
import { NetworkTypes, Status, Utils, Api, } from 'easytier-frontend-lib';
import { NetworkTypes, Status, Utils, Api, ConfigEditDialog } from 'easytier-frontend-lib';
import { watch, computed, onMounted, onUnmounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
@@ -33,6 +33,7 @@ const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
const isEditing = ref(false);
const showCreateNetworkDialog = ref(false);
const showConfigEditDialog = ref(false);
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
const listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined);
@@ -103,7 +104,12 @@ const updateNetworkState = async (disabled: boolean) => {
return;
}
if (disabled || !disabledNetworkConfig.value) {
await props.api?.update_device_instance_state(deviceId.value, selectedInstanceId.value.uuid, disabled);
} else if (disabledNetworkConfig.value) {
await props.api?.delete_network(deviceId.value, disabledNetworkConfig.value.instance_id);
await props.api?.run_network(deviceId.value, disabledNetworkConfig.value);
}
await loadNetworkInstanceIds();
}
@@ -217,9 +223,15 @@ const exportConfig = async () => {
}
try {
let ret = await props.api?.get_network_config(deviceId.value, instanceId.value);
delete ret.instance_id;
exportJsonFile(JSON.stringify(ret, null, 2),instanceId.value +'.json');
let networkConfig = await props.api?.get_network_config(deviceId.value, instanceId.value);
delete networkConfig.instance_id;
let { toml_config: tomlConfig, error } = await props.api?.generate_config({
config: networkConfig
});
if (error) {
throw { response: { data: error } };
}
exportTomlFile(tomlConfig ?? '', instanceId.value + '.toml');
} catch (e: any) {
console.error(e);
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to export network config, error: ' + JSON.stringify(e.response.data), life: 2000 });
@@ -234,30 +246,34 @@ const importConfig = () => {
const handleFileUpload = (event: Event) => {
const files = (event.target as HTMLInputElement).files;
const file = files ? files[0] : null;
if (file) {
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
reader.onload = async (e) => {
try {
let str = e.target?.result?.toString();
if(str){
const config = JSON.parse(str);
if(config === null || typeof config !== "object"){
throw new Error();
let tomlConfig = e.target?.result?.toString();
if (!tomlConfig) return;
const resp = await props.api?.parse_config({ toml_config: tomlConfig });
if (resp.error) {
throw resp.error;
}
Object.assign(newNetworkConfig.value, config);
const config = resp.config;
if (!config) return;
config.instance_id = newNetworkConfig.value?.instance_id ?? config?.instance_id;
Object.assign(newNetworkConfig.value, resp.config);
toast.add({ severity: 'success', summary: 'Import Success', detail: "Config file import success", life: 2000 });
}
} catch (error) {
toast.add({ severity: 'error', summary: 'Error', detail: 'Config file parse error.', life: 2000 });
toast.add({ severity: 'error', summary: 'Error', detail: 'Config file parse error: ' + error, life: 2000 });
}
configFile.value.value = null;
}
reader.readAsText(file);
}
}
const exportJsonFile = (context: string, name: string) => {
let url = window.URL.createObjectURL(new Blob([context], { type: 'application/json' }));
const exportTomlFile = (context: string, name: string) => {
let url = window.URL.createObjectURL(new Blob([context], { type: 'application/toml' }));
let link = document.createElement('a');
link.style.display = 'none';
link.href = url;
@@ -269,6 +285,31 @@ const exportJsonFile = (context: string, name: string) => {
window.URL.revokeObjectURL(url);
}
const generateConfig = async (config: NetworkTypes.NetworkConfig): Promise<string> => {
let { toml_config: tomlConfig, error } = await props.api?.generate_config({ config });
if (error) {
throw error;
}
return tomlConfig ?? '';
}
const saveConfig = async (tomlConfig: string): Promise<void> => {
let resp = await props.api?.parse_config({ toml_config: tomlConfig });
if (resp.error) {
throw resp.error;
};
const config = resp.config;
if (!config) {
throw new Error("Parsed config is empty");
}
config.instance_id = disabledNetworkConfig.value?.instance_id ?? config?.instance_id;
if (networkIsDisabled.value) {
disabledNetworkConfig.value = config;
} else {
newNetworkConfig.value = config;
}
}
let periodFunc = new Utils.PeriodicTask(async () => {
try {
await Promise.all([loadNetworkInstanceIds(), loadDeviceInfo()]);
@@ -288,18 +329,23 @@ onUnmounted(() => {
</script>
<template>
<input type="file" @change="handleFileUpload" class="hidden" accept="application/json" ref="configFile"/>
<input type="file" @change="handleFileUpload" class="hidden" accept="application/toml" ref="configFile" />
<ConfirmPopup></ConfirmPopup>
<Dialog v-model:visible="showCreateNetworkDialog" modal :header="!isEditing ? 'Create New Network' : 'Edit Network'"
:style="{ width: '55rem' }">
<Dialog v-if="!networkIsDisabled" v-model:visible="showCreateNetworkDialog" modal
:header="!isEditing ? 'Create New Network' : 'Edit Network'" :style="{ width: '55rem' }">
<div class="flex flex-col">
<div class="w-11/12 self-center ">
<div class="w-11/12 self-center space-x-2">
<Button @click="showConfigEditDialog = true" icon="pi pi-pen-to-square" label="Edit File" iconPos="right" />
<Button @click="importConfig" icon="pi pi-file-import" label="Import" iconPos="right" />
</div>
</div>
<Divider />
</div>
</div>
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
</Dialog>
<ConfigEditDialog v-if="networkIsDisabled" v-model:visible="showCreateNetworkDialog"
:cur-network="disabledNetworkConfig" :generate-config="generateConfig" :save-config="saveConfig" />
<ConfigEditDialog v-else v-model:visible="showConfigEditDialog" :cur-network="newNetworkConfig"
:generate-config="generateConfig" :save-config="saveConfig" />
<Toolbar>
<template #start>

View File

@@ -11,7 +11,7 @@ use axum::{extract::State, routing::get, Json, Router};
use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
use axum_login::{login_required, AuthManagerLayerBuilder, AuthUser, AuthzBackend};
use axum_messages::MessagesManagerLayer;
use easytier::common::config::ConfigLoader;
use easytier::common::config::{ConfigLoader, TomlConfigLoader};
use easytier::common::scoped_task::ScopedTask;
use easytier::launcher::NetworkConfig;
use easytier::proto::rpc_types;
@@ -68,6 +68,17 @@ struct GenerateConfigResponse {
toml_config: Option<String>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ParseConfigRequest {
toml_config: String,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ParseConfigResponse {
error: Option<String>,
config: Option<NetworkConfig>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Error {
message: String,
@@ -158,6 +169,25 @@ impl RestfulServer {
}
}
async fn handle_parse_config(
Json(req): Json<ParseConfigRequest>,
) -> Result<Json<ParseConfigResponse>, HttpHandleError> {
let config = TomlConfigLoader::new_from_str(&req.toml_config)
.and_then(|config| NetworkConfig::new_from_config(&config));
match config {
Ok(c) => Ok(ParseConfigResponse {
error: None,
config: Some(c),
}
.into()),
Err(e) => Ok(ParseConfigResponse {
error: Some(format!("{:?}", e)),
config: None,
}
.into()),
}
}
pub async fn start(
mut self,
) -> Result<
@@ -216,6 +246,7 @@ impl RestfulServer {
"/api/v1/generate-config",
post(Self::handle_generate_config),
)
.route("/api/v1/parse-config", post(Self::handle_parse_config))
.layer(MessagesManagerLayer)
.layer(auth_layer)
.layer(tower_http::cors::CorsLayer::very_permissive())

View File

@@ -752,4 +752,396 @@ impl NetworkConfig {
cfg.set_flags(flags);
Ok(cfg)
}
pub fn new_from_config(config: &TomlConfigLoader) -> Result<Self, anyhow::Error> {
let default_config = TomlConfigLoader::default();
let mut result = Self::default();
result.instance_id = Some(config.get_id().to_string());
if config.get_hostname() != default_config.get_hostname() {
result.hostname = Some(config.get_hostname());
}
result.dhcp = Some(config.get_dhcp());
let network_identity = config.get_network_identity();
result.network_name = Some(network_identity.network_name.clone());
result.network_secret = network_identity.network_secret.clone();
if let Some(ipv4) = config.get_ipv4() {
result.virtual_ipv4 = Some(ipv4.address().to_string());
result.network_length = Some(ipv4.network_length() as i32);
}
let peers = config.get_peers();
match peers.len() {
1 => {
result.networking_method = Some(NetworkingMethod::PublicServer as i32);
result.public_server_url = Some(peers[0].uri.to_string());
}
0 => {
result.networking_method = Some(NetworkingMethod::Standalone as i32);
}
_ => {
result.networking_method = Some(NetworkingMethod::Manual as i32);
result.peer_urls = peers.iter().map(|p| p.uri.to_string()).collect();
}
}
result.listener_urls = config
.get_listeners()
.unwrap_or_else(|| vec![])
.iter()
.map(|l| l.to_string())
.collect();
result.proxy_cidrs = config
.get_proxy_cidrs()
.iter()
.map(|c| {
if let Some(mapped) = c.mapped_cidr {
format!("{}->{}", c.cidr, mapped)
} else {
c.cidr.to_string()
}
})
.collect();
if let Some(rpc_portal) = config.get_rpc_portal() {
result.rpc_port = Some(rpc_portal.port() as i32);
}
if let Some(whitelist) = config.get_rpc_portal_whitelist() {
result.rpc_portal_whitelists = whitelist.iter().map(|w| w.to_string()).collect();
}
if let Some(vpn_config) = config.get_vpn_portal_config() {
result.enable_vpn_portal = Some(true);
let cidr = vpn_config.client_cidr;
result.vpn_portal_client_network_addr = Some(cidr.first_address().to_string());
result.vpn_portal_client_network_len = Some(cidr.network_length() as i32);
result.vpn_portal_listen_port = Some(vpn_config.wireguard_listen.port() as i32);
}
if let Some(routes) = config.get_routes() {
if !routes.is_empty() {
result.enable_manual_routes = Some(true);
result.routes = routes.iter().map(|r| r.to_string()).collect();
}
}
let exit_nodes = config.get_exit_nodes();
if !exit_nodes.is_empty() {
result.exit_nodes = exit_nodes.iter().map(|n| n.to_string()).collect();
}
if let Some(socks5_portal) = config.get_socks5_portal() {
result.enable_socks5 = Some(true);
result.socks5_port = socks5_portal.port().map(|p| p as i32);
}
let mapped_listeners = config.get_mapped_listeners();
if !mapped_listeners.is_empty() {
result.mapped_listeners = mapped_listeners.iter().map(|l| l.to_string()).collect();
}
let flags = config.get_flags();
result.latency_first = Some(flags.latency_first);
result.dev_name = Some(flags.dev_name.clone());
result.use_smoltcp = Some(flags.use_smoltcp);
result.enable_kcp_proxy = Some(flags.enable_kcp_proxy);
result.disable_kcp_input = Some(flags.disable_kcp_input);
result.enable_quic_proxy = Some(flags.enable_quic_proxy);
result.disable_quic_input = Some(flags.disable_quic_input);
result.disable_p2p = Some(flags.disable_p2p);
result.bind_device = Some(flags.bind_device);
result.no_tun = Some(flags.no_tun);
result.enable_exit_node = Some(flags.enable_exit_node);
result.relay_all_peer_rpc = Some(flags.relay_all_peer_rpc);
result.multi_thread = Some(flags.multi_thread);
result.proxy_forward_by_system = Some(flags.proxy_forward_by_system);
result.disable_encryption = Some(!flags.enable_encryption);
result.disable_udp_hole_punching = Some(flags.disable_udp_hole_punching);
result.enable_magic_dns = Some(flags.accept_dns);
result.mtu = Some(flags.mtu as i32);
result.enable_private_mode = Some(flags.private_mode);
if !flags.relay_network_whitelist.is_empty() && flags.relay_network_whitelist != "*" {
result.enable_relay_network_whitelist = Some(true);
result.relay_network_whitelist = flags
.relay_network_whitelist
.split_whitespace()
.map(|s| s.to_string())
.collect();
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use crate::common::config::ConfigLoader;
use rand::Rng;
use std::net::Ipv4Addr;
fn gen_default_config() -> crate::common::config::TomlConfigLoader {
let config = crate::common::config::TomlConfigLoader::default();
config.set_id(uuid::Uuid::new_v4());
config.set_dhcp(false);
config.set_inst_name("default".to_string());
config.set_listeners(vec![]);
config.set_rpc_portal(std::net::SocketAddr::from(([0, 0, 0, 0], 0)));
config
}
#[test]
fn test_network_config_conversion_basic() -> Result<(), anyhow::Error> {
let config = gen_default_config();
let network_config = super::NetworkConfig::new_from_config(&config)?;
let generated_config = network_config.gen_config()?;
let config_str = config.dump();
let generated_config_str = generated_config.dump();
assert_eq!(
config_str, generated_config_str,
"Generated config does not match original config:\nOriginal:\n{}\n\nGenerated:\n{}\nNetwork Config: {}\n",
config_str, generated_config_str, serde_json::to_string(&network_config).unwrap()
);
Ok(())
}
#[test]
fn test_network_config_conversion_random() -> Result<(), anyhow::Error> {
let mut rng = rand::thread_rng();
for _ in 0..100 {
let config = gen_default_config();
config.set_id(uuid::Uuid::new_v4());
config.set_dhcp(rng.gen_bool(0.5));
if rng.gen_bool(0.7) {
let hostname = format!("host-{}", rng.gen::<u16>());
config.set_hostname(Some(hostname));
}
config.set_network_identity(crate::common::config::NetworkIdentity::new(
format!("network-{}", rng.gen::<u16>()),
format!("secret-{}", rng.gen::<u64>()),
));
config.set_inst_name(config.get_network_identity().network_name.clone());
if !config.get_dhcp() {
let addr = Ipv4Addr::new(
rng.gen_range(1..254),
rng.gen_range(0..255),
rng.gen_range(0..255),
rng.gen_range(1..254),
);
let prefix_len = rng.gen_range(1..31);
let ipv4 = format!("{}/{}", addr, prefix_len).parse().unwrap();
config.set_ipv4(Some(ipv4));
}
let peer_count = rng.gen_range(0..3);
let mut peers = Vec::new();
for _ in 0..peer_count {
let port = rng.gen_range(10000..60000);
let protocol = if rng.gen_bool(0.5) { "tcp" } else { "udp" };
let uri = format!("{}://127.0.0.1:{}", protocol, port)
.parse()
.unwrap();
peers.push(crate::common::config::PeerConfig { uri });
}
config.set_peers(peers);
if rng.gen_bool(0.7) {
let listener_count = rng.gen_range(0..3);
let mut listeners = Vec::new();
for _ in 0..listener_count {
let port = rng.gen_range(10000..60000);
let protocol = if rng.gen_bool(0.5) { "tcp" } else { "udp" };
listeners.push(format!("{}://0.0.0.0:{}", protocol, port).parse().unwrap());
}
config.set_listeners(listeners);
}
if rng.gen_bool(0.6) {
let proxy_count = rng.gen_range(0..3);
for _ in 0..proxy_count {
let network = format!(
"{}.{}.{}.0/{}",
rng.gen_range(1..254),
rng.gen_range(0..255),
rng.gen_range(0..255),
rng.gen_range(24..30)
)
.parse::<cidr::Ipv4Cidr>()
.unwrap();
let mapped_network = if rng.gen_bool(0.5) {
Some(
format!(
"{}.{}.{}.0/{}",
rng.gen_range(1..254),
rng.gen_range(0..255),
rng.gen_range(0..255),
network.network_length()
)
.parse::<cidr::Ipv4Cidr>()
.unwrap(),
)
} else {
None
};
config.add_proxy_cidr(network, mapped_network);
}
}
if rng.gen_bool(0.8) {
let port = rng.gen_range(0..65535);
config.set_rpc_portal(std::net::SocketAddr::from(([0, 0, 0, 0], port)));
if rng.gen_bool(0.6) {
let whitelist_count = rng.gen_range(1..3);
let mut whitelist = Vec::new();
for _ in 0..whitelist_count {
let ip = Ipv4Addr::new(
rng.gen_range(1..254),
rng.gen_range(0..255),
rng.gen_range(0..255),
rng.gen_range(0..255),
);
let cidr = format!("{}/32", ip);
whitelist.push(cidr.parse().unwrap());
}
config.set_rpc_portal_whitelist(Some(whitelist));
}
}
if rng.gen_bool(0.5) {
let vpn_network = format!(
"{}.{}.{}.0/{}",
rng.gen_range(10..173),
rng.gen_range(0..255),
rng.gen_range(0..255),
rng.gen_range(24..30)
);
let vpn_port = rng.gen_range(10000..60000);
config.set_vpn_portal_config(crate::common::config::VpnPortalConfig {
client_cidr: vpn_network.parse().unwrap(),
wireguard_listen: format!("0.0.0.0:{}", vpn_port).parse().unwrap(),
});
}
if rng.gen_bool(0.6) {
let route_count = rng.gen_range(1..3);
let mut routes = Vec::new();
for _ in 0..route_count {
let route = format!(
"{}.{}.{}.0/{}",
rng.gen_range(1..254),
rng.gen_range(0..255),
rng.gen_range(0..255),
rng.gen_range(24..30)
);
routes.push(route.parse().unwrap());
}
config.set_routes(Some(routes));
}
if rng.gen_bool(0.4) {
let node_count = rng.gen_range(1..3);
let mut nodes = Vec::new();
for _ in 0..node_count {
let ip = Ipv4Addr::new(
rng.gen_range(1..254),
rng.gen_range(0..255),
rng.gen_range(0..255),
rng.gen_range(1..254),
);
nodes.push(ip);
}
config.set_exit_nodes(nodes);
}
if rng.gen_bool(0.5) {
let socks5_port = rng.gen_range(10000..60000);
config.set_socks5_portal(Some(
format!("socks5://0.0.0.0:{}", socks5_port).parse().unwrap(),
));
}
if rng.gen_bool(0.4) {
let count = rng.gen_range(1..3);
let mut mapped_listeners = Vec::new();
for _ in 0..count {
let port = rng.gen_range(10000..60000);
mapped_listeners.push(format!("tcp://0.0.0.0:{}", port).parse().unwrap());
}
config.set_mapped_listeners(Some(mapped_listeners));
}
if rng.gen_bool(0.9) {
let mut flags = crate::common::config::gen_default_flags();
flags.latency_first = rng.gen_bool(0.5);
flags.dev_name = format!("etun{}", rng.gen_range(0..10));
flags.use_smoltcp = rng.gen_bool(0.3);
flags.enable_kcp_proxy = rng.gen_bool(0.5);
flags.disable_kcp_input = rng.gen_bool(0.3);
flags.enable_quic_proxy = rng.gen_bool(0.5);
flags.disable_quic_input = rng.gen_bool(0.3);
flags.disable_p2p = rng.gen_bool(0.2);
flags.bind_device = rng.gen_bool(0.3);
flags.no_tun = rng.gen_bool(0.1);
flags.enable_exit_node = rng.gen_bool(0.4);
flags.relay_all_peer_rpc = rng.gen_bool(0.5);
flags.multi_thread = rng.gen_bool(0.7);
flags.proxy_forward_by_system = rng.gen_bool(0.3);
flags.enable_encryption = rng.gen_bool(0.8);
flags.disable_udp_hole_punching = rng.gen_bool(0.2);
flags.accept_dns = rng.gen_bool(0.6);
flags.mtu = rng.gen_range(1200..1500);
flags.private_mode = rng.gen_bool(0.3);
if rng.gen_bool(0.4) {
flags.relay_network_whitelist = (0..rng.gen_range(1..3))
.map(|_| {
format!(
"{}.{}.0.0/16",
rng.gen_range(10..192),
rng.gen_range(0..255)
)
})
.collect::<Vec<_>>()
.join(" ");
}
config.set_flags(flags);
}
let network_config = super::NetworkConfig::new_from_config(&config)?;
let generated_config = network_config.gen_config()?;
generated_config.set_peers(generated_config.get_peers()); // Ensure peers field is not None
let config_str = config.dump();
let generated_config_str = generated_config.dump();
assert_eq!(
config_str, generated_config_str,
"Generated config does not match original config:\nOriginal:\n{}\n\nGenerated:\n{}\nNetwork Config: {}\n",
config_str, generated_config_str, serde_json::to_string(&network_config).unwrap()
);
}
Ok(())
}
}