From ec694f7cb81142592a08229225693baa11c1be33 Mon Sep 17 00:00:00 2001 From: KKRainbow <5665404+KKRainbow@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:30:06 +0800 Subject: [PATCH] fix(web): align central network initial-node protocols (#2704) --- .../frontend-lib/src/components/Config.vue | 15 +---- .../frontend-lib/src/types/network.ts | 14 ++++ .../frontend/src/components/NetworkDetail.vue | 7 +- .../frontend/src/components/NetworkList.vue | 9 +-- .../frontend/tests/dashboard.test.mjs | 64 +++++++++++++++++++ 5 files changed, 82 insertions(+), 27 deletions(-) diff --git a/easytier-web/frontend-lib/src/components/Config.vue b/easytier-web/frontend-lib/src/components/Config.vue index 60bb960b..ea89fef8 100644 --- a/easytier-web/frontend-lib/src/components/Config.vue +++ b/easytier-web/frontend-lib/src/components/Config.vue @@ -10,6 +10,7 @@ import { formFlags, NetworkConfig, normalizeNetworkConfig, + PEER_PROTOCOLS as protos, removeRow, } from '../types/network' import { computed, ref, onMounted, onUnmounted, watch } from 'vue' @@ -84,20 +85,6 @@ const secureModeEnabled = computed({ const { t } = useI18n() -const protos: { [proto: string]: number } = { - tcp: 11010, - udp: 11010, - wg: 11011, - ws: 11011, - wss: 11012, - quic: 11012, - faketcp: 11013, - http: 80, - https: 443, - txt: 0, - srv: 0, -} - const listenerExcludedProtos = new Set(['http', 'https', 'txt', 'srv']) const listenerProtos: { [proto: string]: number } = Object.fromEntries( Object.entries(protos).filter(([proto]) => !listenerExcludedProtos.has(proto)) diff --git a/easytier-web/frontend-lib/src/types/network.ts b/easytier-web/frontend-lib/src/types/network.ts index 9004b1e0..812d51cb 100644 --- a/easytier-web/frontend-lib/src/types/network.ts +++ b/easytier-web/frontend-lib/src/types/network.ts @@ -41,6 +41,20 @@ export { AclAction, AclChainType, AclProtocol, CompressionAlgoPb, NatType, Netwo export { VpnPortalClientState } export type { Acl, AclChain, AclRule, AclV1, GroupIdentity, GroupInfo, NetworkPeerConfig, PeerFeatureFlag, PortForwardConfig, SecureModeConfig, VpnPortalClientConfig, VpnPortalClientInfo, VpnPortalConfig, VpnPortalInfo } +export const PEER_PROTOCOLS: { [proto: string]: number } = { + tcp: 11010, + udp: 11010, + wg: 11011, + ws: 11011, + wss: 11012, + quic: 11012, + faketcp: 11013, + http: 80, + https: 443, + txt: 0, + srv: 0, +} + export type NetworkConfig = Omit< ProtoNetworkConfig, | 'instance_id' diff --git a/easytier-web/frontend/src/components/NetworkDetail.vue b/easytier-web/frontend/src/components/NetworkDetail.vue index b3b41e34..a4f4a4b8 100644 --- a/easytier-web/frontend/src/components/NetworkDetail.vue +++ b/easytier-web/frontend/src/components/NetworkDetail.vue @@ -302,12 +302,7 @@ const savingSettings = ref(false); const gatewayInfo = ref<{ enabled: boolean; peer_url?: string } | undefined>(undefined); -const protos: { [proto: string]: number } = { - tcp: 11010, - udp: 11010, - ws: 11011, - wss: 11012, -}; +const protos = NetworkTypes.PEER_PROTOCOLS; const gatewayFollowed = () => gatewayInfo.value?.enabled === true diff --git a/easytier-web/frontend/src/components/NetworkList.vue b/easytier-web/frontend/src/components/NetworkList.vue index e7e16dd3..2695e5e6 100644 --- a/easytier-web/frontend/src/components/NetworkList.vue +++ b/easytier-web/frontend/src/components/NetworkList.vue @@ -3,7 +3,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'; import { Button, Column, DataTable, Dialog, InputSwitch, InputText, Message, Skeleton, Tag, useToast } from 'primevue'; import { useRoute, useRouter } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { UrlListInput, Utils } from 'easytier-frontend-lib'; +import { NetworkTypes, UrlListInput, Utils } from 'easytier-frontend-lib'; import ApiClient, { type CentralNetworkSettings, type CentralNetworkSummary, type GatewayInfo } from '../modules/api'; import { normalizeVirtualSubnet } from '../modules/virtual-subnet'; @@ -33,12 +33,7 @@ const filteredNetworks = computed(() => { const gatewayEnabled = computed(() => gatewayInfo.value?.enabled === true); -const protos: { [proto: string]: number } = { - tcp: 11010, - udp: 11010, - ws: 11011, - wss: 11012, -}; +const protos = NetworkTypes.PEER_PROTOCOLS; const networkingMethodLabels: { [method: string]: () => string } = { PublicServer: () => t('public_server'), diff --git a/easytier-web/frontend/tests/dashboard.test.mjs b/easytier-web/frontend/tests/dashboard.test.mjs index 522602e4..657b879d 100644 --- a/easytier-web/frontend/tests/dashboard.test.mjs +++ b/easytier-web/frontend/tests/dashboard.test.mjs @@ -243,6 +243,70 @@ test('network creation retains gateway and advanced standalone modes', async t = assert.equal(state.writes.filter(w => w.path === '/networks').at(-1).payload.settings.networking_method, 'Standalone'); }); +for (const action of ['creation', 'settings']) { + test(`network ${action} supports all initial-node protocols and preserves URLs`, async t => { + const { page, state } = await open(t, action === 'creation' ? '/h/networks' : '/h/networks/network-1', {}, state => { + state.gatewayEnabled = false; + state.networks[1].peer_urls = []; + state.networks[1].networking_method = 'Standalone'; + }); + if (action === 'creation') { + await page.getByRole('button', { name: 'Create Network', exact: true }).click(); + await page.locator('#network-display-name').fill('Peer protocols'); + } else { + await page.getByRole('tab', { name: 'Settings', exact: true }).click(); + } + + const peers = [ + 'tcp://peers.example:11010', + 'udp://peers.example:11010', + 'wg://peers.example:11011', + 'ws://peers.example:11011/peers', + 'wss://peers.example:11012/peers', + 'quic://peers.example:11012', + 'faketcp://peers.example:11013', + 'http://peers.example:80/peers', + 'https://peers.example:443/peers', + 'txt://peers.example', + 'srv://_easytier._tcp.peers.example', + ]; + const nodes = page.locator(action === 'creation' ? '#initial-nodes' : '#settings-initial-nodes'); + for (const peer of peers) { + const url = new URL(peer); + const protocol = url.protocol.slice(0, -1); + await nodes.getByText('Add Initial Node', { exact: true }).click(); + const row = nodes.locator('.url-input-full').last(); + await row.locator('.p-autocomplete-dropdown').click(); + const options = page.locator(`#${await row.getByRole('combobox').getAttribute('aria-controls')}`); + await options.getByRole('option', { name: protocol, exact: true }).click(); + await row.locator('input.grow').first().fill(url.hostname); + if (url.pathname) await row.getByPlaceholder('/mypath').fill(url.pathname); + assert.equal(await row.getByRole('spinbutton').count(), ['txt', 'srv'].includes(protocol) ? 0 : 1); + } + + if (action === 'creation') { + await page.getByRole('dialog').getByRole('button', { name: 'Confirm', exact: true }).click(); + await page.waitForURL('**/networks/created-*'); + } else { + const saved = page.waitForResponse(response => response.request().method() === 'PATCH'); + await page.getByRole('button', { name: 'Save', exact: true }).click(); + await saved; + } + let settings = state.writes.at(-1).payload.settings; + assert.equal(settings.networking_method, 'Manual'); + assert.deepEqual(settings.peer_urls, peers); + + await page.reload(); + await page.getByRole('tab', { name: 'Settings', exact: true }).click(); + const saved = page.waitForResponse(response => response.request().method() === 'PATCH'); + await page.getByRole('button', { name: 'Save', exact: true }).click(); + await saved; + settings = state.writes.at(-1).payload.settings; + assert.equal(settings.networking_method, 'Manual'); + assert.deepEqual(settings.peer_urls, peers); + }); +} + for (const [input, expected] of [ [' 10.200.0.0 ', '10.200.0.0/24'], [' 10.200.0.0/16 ', '10.200.0.0/16'],