From 97e2c9e7ba8d3d80b095edc3f2dbeb7399b56dca Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Thu, 2 Jul 2026 17:37:04 +0200 Subject: [PATCH] fix(web): sync the VLESS generate-key dropdown with the encryption field The auth-kind dropdown in the VLESS "Generate Key" block was hardcoded to x25519 on mount, while the "Already selected" text next to it was derived independently from settings.encryption. Editing an inbound whose encryption uses another kind (e.g. ML-KEM-768) showed a mismatched dropdown, and clicking Generate without noticing would produce a keypair of the wrong kind for the inbound. Extract the encryption-string parsing into a shared pure helper (lib/xray/vless-encryption), use it both for the selected-auth label and to initialize/sync the dropdown, so the two can no longer diverge. When the encryption is none or unparseable the dropdown keeps its x25519 default. Closes #5744 --- frontend/src/lib/xray/vless-encryption.ts | 29 +++++++++++++++++++ .../pages/inbounds/form/InboundFormModal.tsx | 26 +++++------------ .../pages/inbounds/form/protocols/vless.tsx | 29 ++++++++----------- frontend/src/test/vless-encryption.test.ts | 27 +++++++++++++++++ 4 files changed, 75 insertions(+), 36 deletions(-) create mode 100644 frontend/src/lib/xray/vless-encryption.ts create mode 100644 frontend/src/test/vless-encryption.test.ts diff --git a/frontend/src/lib/xray/vless-encryption.ts b/frontend/src/lib/xray/vless-encryption.ts new file mode 100644 index 000000000..7c51daac7 --- /dev/null +++ b/frontend/src/lib/xray/vless-encryption.ts @@ -0,0 +1,29 @@ +export type VlessAuthKind = + | 'x25519' + | 'x25519_xorpub' + | 'x25519_random' + | 'mlkem768' + | 'mlkem768_xorpub' + | 'mlkem768_random'; + +export const VLESS_AUTH_LABEL_KEYS: Record = { + x25519: 'pages.inbounds.vlessAuthX25519', + x25519_xorpub: 'pages.inbounds.vlessAuthX25519Xorpub', + x25519_random: 'pages.inbounds.vlessAuthX25519Random', + mlkem768: 'pages.inbounds.vlessAuthMlkem768', + mlkem768_xorpub: 'pages.inbounds.vlessAuthMlkem768Xorpub', + mlkem768_random: 'pages.inbounds.vlessAuthMlkem768Random', +}; + +const MLKEM768_MIN_KEY_LENGTH = 300; + +export function vlessEncryptionAuthKind(encryption: string): VlessAuthKind | null { + if (!encryption || encryption === 'none') return null; + const parts = encryption.split('.').filter(Boolean); + const authKey = parts[parts.length - 1] || ''; + if (!authKey) return null; + const mode = parts[1] || 'native'; + const keyType = authKey.length > MLKEM768_MIN_KEY_LENGTH ? 'mlkem768' : 'x25519'; + if (mode === 'xorpub' || mode === 'random') return `${keyType}_${mode}`; + return keyType; +} diff --git a/frontend/src/pages/inbounds/form/InboundFormModal.tsx b/frontend/src/pages/inbounds/form/InboundFormModal.tsx index 850029932..5e9130e74 100644 --- a/frontend/src/pages/inbounds/form/InboundFormModal.tsx +++ b/frontend/src/pages/inbounds/form/InboundFormModal.tsx @@ -41,6 +41,7 @@ import { Protocols } from '@/schemas/primitives'; import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt'; import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria'; import { createHysteriaTlsSettingsWithDefaultCert } from '@/lib/xray/inbound-tls-defaults'; +import { VLESS_AUTH_LABEL_KEYS, vlessEncryptionAuthKind } from '@/lib/xray/vless-encryption'; import { SniffingSchema } from '@/schemas/primitives/sniffing'; import { TcpStreamSettingsSchema } from '@/schemas/protocols/stream/tcp'; import { KcpStreamSettingsSchema } from '@/schemas/protocols/stream/kcp'; @@ -317,27 +318,14 @@ export default function InboundFormModal({ form.setFieldValue(['settings', 'encryption'], 'none'); }; + const vlessAuthKind = vlessEncryptionAuthKind( + typeof vlessEncryption === 'string' ? vlessEncryption : '', + ); const selectedVlessAuth = (() => { const enc = typeof vlessEncryption === 'string' ? vlessEncryption : ''; if (!enc || enc === 'none') return 'None'; - const parts = enc.split('.').filter(Boolean); - const authKey = parts[parts.length - 1] || ''; - if (!authKey) return t('pages.inbounds.vlessAuthCustom'); - const mode = parts[1] || 'native'; - const keyType = authKey.length > 300 ? 'mlkem768' : 'x25519'; - if (mode === 'xorpub') { - return keyType === 'mlkem768' - ? t('pages.inbounds.vlessAuthMlkem768Xorpub') - : t('pages.inbounds.vlessAuthX25519Xorpub'); - } - if (mode === 'random') { - return keyType === 'mlkem768' - ? t('pages.inbounds.vlessAuthMlkem768Random') - : t('pages.inbounds.vlessAuthX25519Random'); - } - return keyType === 'mlkem768' - ? t('pages.inbounds.vlessAuthMlkem768') - : t('pages.inbounds.vlessAuthX25519'); + if (!vlessAuthKind) return t('pages.inbounds.vlessAuthCustom'); + return t(VLESS_AUTH_LABEL_KEYS[vlessAuthKind]); })(); useEffect(() => { @@ -703,7 +691,7 @@ export default function InboundFormModal({ {protocol === Protocols.SHADOWSOCKS && } - {protocol === Protocols.VLESS && } + {protocol === Protocols.VLESS && } {isFallbackHost && fallbacksCard} {(protocol === Protocols.VLESS || protocol === Protocols.TROJAN) diff --git a/frontend/src/pages/inbounds/form/protocols/vless.tsx b/frontend/src/pages/inbounds/form/protocols/vless.tsx index 310bc8ffe..85c029b34 100644 --- a/frontend/src/pages/inbounds/form/protocols/vless.tsx +++ b/frontend/src/pages/inbounds/form/protocols/vless.tsx @@ -1,18 +1,13 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Form, Input, InputNumber, Select, Space, Typography } from 'antd'; -type VlessAuthKind = - | 'x25519' - | 'x25519_xorpub' - | 'x25519_random' - | 'mlkem768' - | 'mlkem768_xorpub' - | 'mlkem768_random'; +import { VLESS_AUTH_LABEL_KEYS, type VlessAuthKind } from '@/lib/xray/vless-encryption'; interface VlessFieldsProps { saving: boolean; selectedVlessAuth: string; + vlessAuthKind: VlessAuthKind | null; network: string; security: string; getNewVlessEnc: (kind: VlessAuthKind) => void; @@ -22,22 +17,22 @@ interface VlessFieldsProps { export default function VlessFields({ saving, selectedVlessAuth, + vlessAuthKind, network, security, getNewVlessEnc, clearVlessEnc, }: VlessFieldsProps) { const { t } = useTranslation(); - const [authKind, setAuthKind] = useState('x25519'); + const [authKind, setAuthKind] = useState(vlessAuthKind ?? 'x25519'); - const authOptions = [ - { value: 'x25519', label: t('pages.inbounds.vlessAuthX25519') }, - { value: 'x25519_xorpub', label: t('pages.inbounds.vlessAuthX25519Xorpub') }, - { value: 'x25519_random', label: t('pages.inbounds.vlessAuthX25519Random') }, - { value: 'mlkem768', label: t('pages.inbounds.vlessAuthMlkem768') }, - { value: 'mlkem768_xorpub', label: t('pages.inbounds.vlessAuthMlkem768Xorpub') }, - { value: 'mlkem768_random', label: t('pages.inbounds.vlessAuthMlkem768Random') }, - ]; + useEffect(() => { + setAuthKind(vlessAuthKind ?? 'x25519'); + }, [vlessAuthKind]); + + const authOptions = (Object.entries(VLESS_AUTH_LABEL_KEYS) as [VlessAuthKind, string][]).map( + ([value, labelKey]) => ({ value, label: t(labelKey) }), + ); return ( <> diff --git a/frontend/src/test/vless-encryption.test.ts b/frontend/src/test/vless-encryption.test.ts new file mode 100644 index 000000000..be0052449 --- /dev/null +++ b/frontend/src/test/vless-encryption.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest'; + +import { vlessEncryptionAuthKind } from '@/lib/xray/vless-encryption'; + +const x25519Key = 'kO9pIKKPtoUCzo3ZWfWfp0lQoWCyJC1TqL8oz1hpsFM'; +const mlkem768Key = 'A'.repeat(1590); + +describe('vlessEncryptionAuthKind', () => { + const cases: { name: string; encryption: string; want: ReturnType }[] = [ + { name: 'empty string', encryption: '', want: null }, + { name: 'none', encryption: 'none', want: null }, + { name: 'only dots', encryption: '...', want: null }, + { name: 'x25519 native', encryption: `mlkem768x25519plus.native.600s.${x25519Key}`, want: 'x25519' }, + { name: 'x25519 xorpub', encryption: `mlkem768x25519plus.xorpub.600s.${x25519Key}`, want: 'x25519_xorpub' }, + { name: 'x25519 random', encryption: `mlkem768x25519plus.random.600s.${x25519Key}`, want: 'x25519_random' }, + { name: 'mlkem768 native', encryption: `mlkem768x25519plus.native.600s.${mlkem768Key}`, want: 'mlkem768' }, + { name: 'mlkem768 xorpub', encryption: `mlkem768x25519plus.xorpub.600s.${mlkem768Key}`, want: 'mlkem768_xorpub' }, + { name: 'mlkem768 random', encryption: `mlkem768x25519plus.random.600s.${mlkem768Key}`, want: 'mlkem768_random' }, + { name: 'two-segment value treated as native', encryption: `mlkem768x25519plus.${x25519Key}`, want: 'x25519' }, + ]; + + for (const c of cases) { + it(c.name, () => { + expect(vlessEncryptionAuthKind(c.encryption)).toBe(c.want); + }); + } +});