From 4e616129446cd26c035ac608975a43271a60e4e5 Mon Sep 17 00:00:00 2001 From: KKRainbow <443152178@qq.com> Date: Mon, 29 Jun 2026 12:40:04 +0800 Subject: [PATCH] fix(web): preserve managed config and status compatibility (#2389) Fix web/frontend compat bugs in managed config & runtime status - Preserve [[peer]].peer_public_key when TOML configs round-trip through the web/managed NetworkConfig path - Keep old peer_urls clients working while adding structured peer metadata for new clients - Make frontend protobuf JSON normalization preserve omitted-field semantics instead of turning missing data into misleading defaults - Harden runtime status rendering against omitted or string-encoded backend fields - Expose peer-route feature flags in the web status UI --- easytier-gui/src/composables/mobile_vpn.ts | 25 ++- easytier-gui/src/modules/api.ts | 4 +- .../scripts/test-network-config.mjs | 62 +++++++ .../src/components/RemoteManagement.vue | 43 +++-- .../frontend-lib/src/components/Status.vue | 62 ++++--- .../src/components/acl/AclChainEditor.vue | 31 ++-- .../src/components/acl/AclGroupEditor.vue | 29 ++- .../src/components/acl/AclManager.vue | 41 ++-- .../src/components/acl/AclRuleDialog.vue | 6 +- easytier-web/frontend-lib/src/locales/cn.yaml | 2 + easytier-web/frontend-lib/src/locales/en.yaml | 2 + .../frontend-lib/src/modules/statusDisplay.ts | 82 ++++++++ .../frontend-lib/src/modules/utils.ts | 44 +++-- .../frontend-lib/src/types/network.ts | 175 ++++++++++++++++-- .../frontend-lib/tests/status-display.spec.ts | 84 +++++++++ easytier-web/frontend/src/modules/api.ts | 2 +- easytier/src/launcher.rs | 129 +++++++++++-- easytier/src/proto/api_manage.proto | 6 + 18 files changed, 665 insertions(+), 164 deletions(-) create mode 100644 easytier-web/frontend-lib/src/modules/statusDisplay.ts create mode 100644 easytier-web/frontend-lib/tests/status-display.spec.ts diff --git a/easytier-gui/src/composables/mobile_vpn.ts b/easytier-gui/src/composables/mobile_vpn.ts index a5332608..d9987642 100644 --- a/easytier-gui/src/composables/mobile_vpn.ts +++ b/easytier-gui/src/composables/mobile_vpn.ts @@ -163,14 +163,10 @@ async function registerVpnServiceListener() { ) } -function getRoutesForVpn(routes: Route[], node_config: NetworkTypes.NetworkConfig): string[] { - if (!routes) { - return [] - } - +function getRoutesForVpn(routes: Route[] | undefined, node_config: NetworkTypes.NetworkConfig): string[] { const ret = [] - for (const r of routes) { - for (let cidr of r.proxy_cidrs) { + for (const r of routes ?? []) { + for (let cidr of r.proxy_cidrs ?? []) { if (!cidr.includes('/')) { cidr += '/32' } @@ -178,9 +174,9 @@ function getRoutesForVpn(routes: Route[], node_config: NetworkTypes.NetworkConfi } } - node_config.routes.forEach(r => { - ret.push(r) - }) + for (const route of node_config.routes ?? []) { + ret.push(route) + } if (node_config.enable_magic_dns) { ret.push('100.100.100.101/32') @@ -215,14 +211,15 @@ export async function onNetworkInstanceChange(instanceId: string) { console.log('vpn service skipped because no_tun is enabled', instanceId) return } - const curNetworkInfo = (await collectNetworkInfo(instanceId)).info.map[instanceId] + const curNetworkInfo = (await collectNetworkInfo(instanceId))?.info?.map?.[instanceId] if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) { console.warn('vpn service skipped because network info is unavailable', instanceId, curNetworkInfo?.error_msg) await doStopVpn() return } - const virtual_ip = Utils.ipv4ToString(curNetworkInfo?.my_node_info?.virtual_ipv4.address) + const virtualIpv4 = curNetworkInfo.my_node_info?.virtual_ipv4 + const virtual_ip = virtualIpv4?.address?.addr ? Utils.ipv4ToString(virtualIpv4.address) : undefined if (config.dhcp && (!virtual_ip || !virtual_ip.length)) { console.log('DHCP enabled but no IP yet, will retry in', DHCP_POLLING_INTERVAL, 'ms') @@ -237,7 +234,7 @@ export async function onNetworkInstanceChange(instanceId: string) { return } - let network_length = curNetworkInfo?.my_node_info?.virtual_ipv4.network_length + let network_length = virtualIpv4?.network_length if (!network_length) { network_length = 24 } @@ -290,7 +287,7 @@ async function isNoTunEnabled(instanceId: string | undefined) { async function findRunningTunInstanceId() { const instanceIds = await listNetworkInstanceIds() - const runningIds = instanceIds.running_inst_ids.map(Utils.UuidToStr) + const runningIds = (instanceIds.running_inst_ids ?? []).map(Utils.UuidToStr) console.log('vpn service sync running instances', JSON.stringify(runningIds)) for (const instanceId of runningIds) { diff --git a/easytier-gui/src/modules/api.ts b/easytier-gui/src/modules/api.ts index 627206e3..d24c27e6 100644 --- a/easytier-gui/src/modules/api.ts +++ b/easytier-gui/src/modules/api.ts @@ -9,7 +9,7 @@ export class GUIRemoteClient implements Api.RemoteClient { await backend.runNetworkInstance(config, save); } async get_network_info(inst_id: string): Promise { - return backend.collectNetworkInfo(inst_id).then(infos => infos.info.map[inst_id]); + return backend.collectNetworkInfo(inst_id).then(infos => infos.info?.map?.[inst_id]); } async list_network_instance_ids(): Promise { return backend.listNetworkInstanceIds(); @@ -44,4 +44,4 @@ export class GUIRemoteClient implements Api.RemoteClient { return await backend.getNetworkMetas(instance_ids); } -} \ No newline at end of file +} diff --git a/easytier-web/frontend-lib/scripts/test-network-config.mjs b/easytier-web/frontend-lib/scripts/test-network-config.mjs index 5251e155..a996427d 100644 --- a/easytier-web/frontend-lib/scripts/test-network-config.mjs +++ b/easytier-web/frontend-lib/scripts/test-network-config.mjs @@ -114,6 +114,12 @@ function allFieldFixture() { networking_method: NetworkingMethod.Manual, public_server_url: 'tcp://public.example:11010', peer_urls: [' tcp://peer-a:11010 ', '', 'udp://peer-b:11010'], + peers: [ + { + uri: 'tcp://peer-a:11010', + peer_public_key: 'peer-a-public-key', + }, + ], proxy_cidrs: ['10.10.0.0/16', '192.168.2.0/24->10.99.0.0/24'], enable_vpn_portal: true, vpn_portal_listen_port: 23000, @@ -259,6 +265,8 @@ function assertFullFieldRoundTrip() { assert.equal(backend.networking_method, 'Manual') assert.equal(backend.public_server_url, '') assert.deepEqual(backend.peer_urls, ['tcp://peer-a:11010', 'udp://peer-b:11010']) + assert.equal(backend.peers[0].peer_public_key, 'peer-a-public-key') + assert.deepEqual(backend.peers[1], { uri: 'udp://peer-b:11010' }) assert.equal(backend.data_compress_algo, 'Zstd') assert.equal(backend.instance_recv_bps_limit, '9007199254740993') assert.equal(backend.secure_mode.enabled, true) @@ -415,6 +423,59 @@ function assertNetworkingMethodNormalization() { }) assert.deepEqual(missing.peer_urls, ['tcp://one', 'udp://two']) + + const publicServerMissingUrl = normalizeNetworkConfig({ + ...DEFAULT_NETWORK_CONFIG(), + networking_method: 'PublicServer', + public_server_url: '', + peer_urls: ['tcp://manual.example:11010'], + }) + + assert.deepEqual(publicServerMissingUrl.peer_urls, []) +} + +function assertPeerPublicKeysPreserved() { + const normalized = normalizeNetworkConfig({ + ...DEFAULT_NETWORK_CONFIG(), + peer_urls: [], + peers: [ + { + uri: ' tcp://peer-a:11010 ', + peer_public_key: 'peer-a-public-key', + }, + ], + }) + + assert.deepEqual(normalized.peer_urls, ['tcp://peer-a:11010']) + assert.deepEqual(normalized.peers, [ + { + uri: 'tcp://peer-a:11010', + peer_public_key: 'peer-a-public-key', + }, + ]) + + const unchangedUrl = toBackendNetworkConfig({ + ...normalized, + peer_urls: ['tcp://peer-a:11010', 'tcp://peer-b:11010'], + }) + + assert.equal(unchangedUrl.peers[0].peer_public_key, 'peer-a-public-key') + assert.deepEqual(unchangedUrl.peers[1], { uri: 'tcp://peer-b:11010' }) + + const changedUrl = toBackendNetworkConfig({ + ...normalized, + peer_urls: ['tcp://peer-c:11010'], + }) + + assert.deepEqual(changedUrl.peers, [{ uri: 'tcp://peer-c:11010' }]) + + const clearedUrls = toBackendNetworkConfig({ + ...normalized, + peer_urls: [], + }) + + assert.deepEqual(clearedUrls.peer_urls ?? [], []) + assert.deepEqual(clearedUrls.peers ?? [], []) } function assertNumberBoundaries() { @@ -469,6 +530,7 @@ const tests = [ assertEnumCompatibility, assertAclDefaultsAndExplicitZero, assertNetworkingMethodNormalization, + assertPeerPublicKeysPreserved, assertNumberBoundaries, ] diff --git a/easytier-web/frontend-lib/src/components/RemoteManagement.vue b/easytier-web/frontend-lib/src/components/RemoteManagement.vue index 3d2393fa..1ddc2b8a 100644 --- a/easytier-web/frontend-lib/src/components/RemoteManagement.vue +++ b/easytier-web/frontend-lib/src/components/RemoteManagement.vue @@ -35,7 +35,7 @@ const currentNetworkConfig = ref(undefin const listInstanceIdResponse = ref(undefined); const isRunning = (instanceId: string) => { - return listInstanceIdResponse.value?.running_inst_ids.map(Utils.UuidToStr).includes(instanceId); + return (listInstanceIdResponse.value?.running_inst_ids ?? []).map(Utils.UuidToStr).includes(instanceId); } const networkMetaCache = ref>({}); @@ -46,7 +46,7 @@ const loadNetworkMetas = async (instanceIds: string[]) => { try { const response = await props.api.get_network_metas(missingIds); - Object.assign(networkMetaCache.value, response.metas); + Object.assign(networkMetaCache.value, response.metas ?? {}); } catch (e) { console.error("Failed to load network metas", e); } @@ -80,8 +80,8 @@ const updateInstanceList = () => { let insts = new Set(); let t = listInstanceIdResponse.value; if (t) { - t.running_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u))); - t.disabled_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u))); + (t.running_inst_ids ?? []).forEach((u) => insts.add(Utils.UuidToStr(u))); + (t.disabled_inst_ids ?? []).forEach((u) => insts.add(Utils.UuidToStr(u))); } const newList = Array.from(insts).map((instance: string) => { @@ -149,7 +149,7 @@ const networkIsDisabled = computed(() => { if (!selectedInstanceId.value) { return false; } - return listInstanceIdResponse.value?.disabled_inst_ids.map(Utils.UuidToStr).includes(selectedInstanceId.value?.uuid); + return (listInstanceIdResponse.value?.disabled_inst_ids ?? []).map(Utils.UuidToStr).includes(selectedInstanceId.value?.uuid); }); watch(networkIsDisabled, async (newVal, oldVal) => { if (newVal !== oldVal && newVal === true) { @@ -287,17 +287,35 @@ const loadNetworkInstanceIds = async () => { } const loadCurrentNetworkInfo = async () => { - if (!selectedInstanceId.value) { + const selected = selectedInstanceId.value?.uuid; + if (!selected) { + curNetworkInfo.value = null; return; } if (!needShowNetworkStatus.value) { + curNetworkInfo.value = null; + return; + } + if (curNetworkInfo.value?.instance_id !== selected) { + curNetworkInfo.value = null; + } + + let network_info = await props.api.get_network_info(selected); + if (selectedInstanceId.value?.uuid !== selected) { return; } - let network_info = await props.api.get_network_info(selectedInstanceId.value.uuid); + if (!network_info) { + curNetworkInfo.value = { + instance_id: selected, + running: false, + error_msg: t('web.device_management.network_info_unavailable'), + } as NetworkTypes.NetworkInstance; + return; + } curNetworkInfo.value = { - instance_id: selectedInstanceId.value.uuid, + instance_id: selected, running: network_info?.running ?? false, error_msg: network_info?.error_msg ?? '', detail: network_info, @@ -492,7 +510,7 @@ onUnmounted(() => {
{{ t('network_name') }}: {{ - slotProps.option.meta.network_name }} + slotProps.option.meta?.network_name ?? slotProps.option.uuid }}
{

{{ t('web.device_management.network_status') }}

- - {{ curNetworkInfo?.error_msg }} + {{ + curNetworkInfo.error_msg }} + {{ t('web.device_management.loading_network_status') }} +