diff --git a/frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx b/frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx index 83e3154d2..537cddd89 100644 --- a/frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx +++ b/frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx @@ -4,7 +4,9 @@ import type { FormInstance } from 'antd/es/form'; import type { NamePath } from 'antd/es/form/interface'; import { RandomUtil } from '@/utils'; -import { OutboundProtocols } from '@/schemas/primitives'; +import { OutboundProtocols, UTLS_FINGERPRINT } from '@/schemas/primitives'; + +const UTLS_FINGERPRINT_OPTIONS = Object.values(UTLS_FINGERPRINT).map((value) => ({ value, label: value })); export interface FinalMaskFormProps { name: NamePath; @@ -18,6 +20,46 @@ export interface FinalMaskFormProps { } const TCP_NETWORKS = ['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp']; +const DEFAULT_GECKO_PACKET_SIZE = { min: 512, max: 1200 }; +// Xray-core caps the Gecko output packet size at its internal buffer (2048) +// and needs 1 <= min <= max; mirror those bounds so the panel rejects what +// core would reject at runtime (salamander/conn.go). +const GECKO_MIN_PACKET_SIZE = 1; +const GECKO_MAX_PACKET_SIZE = 2048; + +export function parseGeckoPacketSize(value: unknown): { min: number; max: number } | null { + const str = typeof value === 'string' ? value.trim() : String(value ?? '').trim(); + const match = /^(\d+)-(\d+)$/.exec(str); + if (!match) return null; + const min = Number(match[1]); + const max = Number(match[2]); + if ( + !Number.isSafeInteger(min) || !Number.isSafeInteger(max) + || min < GECKO_MIN_PACKET_SIZE || max < min || max > GECKO_MAX_PACKET_SIZE + ) { + return null; + } + return { min, max }; +} + +function formatGeckoPacketSize(min: number, max: number): string { + return `${min}-${max}`; +} + +function splitGeckoPacketSize(value: unknown): { min: number | null; max: number | null } { + const str = typeof value === 'string' ? value.trim() : String(value ?? '').trim(); + const [minRaw = '', maxRaw = ''] = str.split('-', 2); + const min = /^\d+$/.test(minRaw) ? Number(minRaw) : null; + const max = /^\d+$/.test(maxRaw) ? Number(maxRaw) : null; + return { min, max }; +} + +function validateGeckoPacketSize(_rule: unknown, value: unknown): Promise { + if (parseGeckoPacketSize(value)) return Promise.resolve(); + return Promise.reject(new Error( + `Use a range like 512-1200 (${GECKO_MIN_PACKET_SIZE}-${GECKO_MAX_PACKET_SIZE}, max ≥ min)`, + )); +} function asPath(name: NamePath): (string | number)[] { return Array.isArray(name) ? [...name] : [name]; @@ -470,22 +512,7 @@ function UdpMaskItem({ {({ getFieldValue }) => { const type = getFieldValue([...absolutePath, 'type']) as string | undefined; if (type === 'salamander') { - return ( - - - - - -