diff --git a/frontend/src/lib/xray/forms/SniffingFields.tsx b/frontend/src/lib/xray/forms/SniffingFields.tsx new file mode 100644 index 000000000..139b2a761 --- /dev/null +++ b/frontend/src/lib/xray/forms/SniffingFields.tsx @@ -0,0 +1,63 @@ +import { useTranslation } from 'react-i18next'; +import { Form, Select, Switch } from 'antd'; +import type { FormInstance } from 'antd/es/form'; + +import { SNIFFING_OPTION } from '@/schemas/primitives'; + +const DEST_OPTIONS = Object.entries(SNIFFING_OPTION).map(([label, value]) => ({ value, label })); + +export interface SniffingFieldsProps { + // Base path to the sniffing object in the form, e.g. ['sniffing'] (inbound), + // ['settings', 'reverseSniffing'] (VLESS reverse), ['settings', 'sniffing'] + // (loopback). All sub-fields hang off this path. + name: (string | number)[]; + form: FormInstance; + // Label for the enable toggle — Enable / Reverse Sniffing / Sniffing differ + // per host. + enableLabel: string; +} + +// Shared sniffing form fragment used everywhere the panel edits an xray +// SniffingConfig: the inbound Sniffing tab, VLESS reverse sniffing, and the +// loopback outbound. Renders the enable toggle plus the destOverride / +// metadataOnly / routeOnly / excluded fields when enabled. +export default function SniffingFields({ name, form, enableLabel }: SniffingFieldsProps) { + const { t } = useTranslation(); + const enabled = Form.useWatch([...name, 'enabled'], form) ?? false; + + return ( + <> + + + + + {enabled && ( + <> + + + + + - - - - + + @@ -321,9 +376,6 @@ function validateFragmentPackets(_rule: unknown, value: unknown): Promise return Promise.reject(new Error('Use "tlshello" or a packet range like 1-3')); } -// Walks a deep object path safely. Used inside shouldUpdate which gets -// the whole form values blob; we need to compare a deep field across -// prev/curr without crashing on missing intermediates. function validateFragmentLength(_rule: unknown, value: unknown): Promise { const str = typeof value === 'string' ? value.trim() : String(value ?? '').trim(); if (str.length === 0) { @@ -336,6 +388,61 @@ function validateFragmentLength(_rule: unknown, value: unknown): Promise { return Promise.resolve(); } +// A delay segment is a millisecond value or range; 0 is allowed (no delay), +// but an empty row would serialize as "" and break xray's Int32Range parse, +// so require a value and let the user remove the row instead. +function validateFragmentDelayEntry(_rule: unknown, value: unknown): Promise { + const str = typeof value === 'string' ? value.trim() : String(value ?? '').trim(); + if (str.length === 0) { + return Promise.reject(new Error("Delay is required — remove the row if you don't want a delay")); + } + if (!/^\d+(?:-\d+)?$/.test(str)) { + return Promise.reject(new Error('Use a delay in ms, e.g. 10 or 10-20')); + } + return Promise.resolve(); +} + +// Per-segment range list for a fragment mask's `lengths`/`delays` (xray-core +// #6334): an editable list of dash-range strings. xray applies entry N to +// fragment segment N, clamping to the last entry. `minItems` keeps at least +// one length row so the config never collapses to an empty (rejected) list. +function FragmentRangeList({ + listName, label, placeholder, validator, minItems = 0, +}: { + listName: (string | number)[]; + label: string; + placeholder: string; + validator?: (rule: unknown, value: unknown) => Promise; + minItems?: number; +}) { + return ( + + {(fields, { add, remove }) => ( + <> + +