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); + }); + } +});