feat(xray): add loopback sniffing and per-segment fragment masks

- Loopback outbound: add sniffing support (xray-core #6320)

- FinalMask fragment: support per-segment lengths/delays arrays with legacy length/delay migration (xray-core #6334)

- Consolidate sniffing into a shared SniffingFields component and the canonical SniffingSchema across inbound, VLESS reverse, and loopback
This commit is contained in:
MHSanaei
2026-06-23 13:24:16 +02:00
parent 42cd351e4e
commit 852b53db79
18 changed files with 343 additions and 255 deletions
@@ -193,7 +193,6 @@ export default function InboundFormModal({
// actually live on a node — otherwise the node address it would resolve to is
// always empty. Offer it only then; `listen`/`custom` work for local inbounds.
const nodeShareOptionAvailable = selectableNodes.length > 0 && isNodeEligible;
const sniffingEnabled = Form.useWatch(['sniffing', 'enabled'], form) ?? false;
const vlessEncryption = Form.useWatch(['settings', 'encryption'], form) ?? '';
const ssMethod = Form.useWatch(['settings', 'method'], form);
const isSSWith2022 = isSS2022({
@@ -977,7 +976,7 @@ export default function InboundFormModal({
</div>
);
const sniffingTab = <SniffingTab sniffingEnabled={sniffingEnabled} />;
const sniffingTab = <SniffingTab />;
return (
<>
@@ -1,67 +1,16 @@
import { useTranslation } from 'react-i18next';
import { Checkbox, Form, Select, Switch } from 'antd';
import { Form } from 'antd';
import { SNIFFING_OPTION } from '@/schemas/primitives';
import SniffingFields from '@/lib/xray/forms/SniffingFields';
export default function SniffingTab({ sniffingEnabled }: { sniffingEnabled: boolean }) {
export default function SniffingTab() {
const { t } = useTranslation();
const form = Form.useFormInstance();
return (
<>
<Form.Item name={['sniffing', 'enabled']} label={t('enable')} valuePropName="checked">
<Switch />
</Form.Item>
{sniffingEnabled && (
<>
<Form.Item name={['sniffing', 'destOverride']} wrapperCol={{ span: 24 }}>
<Checkbox.Group>
{Object.entries(SNIFFING_OPTION).map(([key, value]) => (
<Checkbox key={key} value={value}>{key}</Checkbox>
))}
</Checkbox.Group>
</Form.Item>
<Form.Item
name={['sniffing', 'metadataOnly']}
label={t('pages.inbounds.sniffingMetadataOnly')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['sniffing', 'routeOnly']}
label={t('pages.inbounds.sniffingRouteOnly')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['sniffing', 'ipsExcluded']}
label={t('pages.inbounds.sniffingIpsExcluded')}
>
<Select
mode="tags"
tokenSeparators={[',']}
placeholder="IP/CIDR/geoip:*/ext:*"
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item
name={['sniffing', 'domainsExcluded']}
label={t('pages.inbounds.sniffingDomainsExcluded')}
>
<Select
mode="tags"
tokenSeparators={[',']}
placeholder="domain:*/ext:*"
style={{ width: '100%' }}
/>
</Form.Item>
</>
)}
</>
<SniffingFields
name={['sniffing']}
form={form}
enableLabel={t('enable')}
/>
);
}
@@ -8,11 +8,11 @@ import {
Radio,
Select,
Space,
Switch,
Tabs,
message,
} from 'antd';
import { FinalMaskForm } from '@/lib/xray/forms/transport';
import SniffingFields from '@/lib/xray/forms/SniffingFields';
import { JsonEditor } from '@/components/form';
import { Wireguard } from '@/utils';
import {
@@ -25,7 +25,6 @@ import {
OutboundFormBaseSchema,
type OutboundFormValues,
} from '@/schemas/forms/outbound-form';
import { SNIFFING_OPTION } from '@/schemas/primitives';
import {
canEnableReality,
canEnableStream,
@@ -412,70 +411,12 @@ export default function OutboundFormModal({
{() => {
const reverseTag = form.getFieldValue(['settings', 'reverseTag']);
if (!reverseTag) return null;
const sniff = (form.getFieldValue(['settings', 'reverseSniffing']) ?? {}) as {
enabled?: boolean;
};
return (
<>
<Form.Item
label={t('pages.xray.outboundForm.reverseSniffing')}
name={['settings', 'reverseSniffing', 'enabled']}
valuePropName="checked"
>
<Switch />
</Form.Item>
{sniff.enabled && (
<>
<Form.Item
wrapperCol={{ md: { span: 14, offset: 8 } }}
name={['settings', 'reverseSniffing', 'destOverride']}
>
<Select
mode="multiple"
className="sniffing-options"
options={Object.entries(SNIFFING_OPTION).map(([k, v]) => ({
value: v,
label: k,
}))}
/>
</Form.Item>
<Form.Item
label={t('pages.inbounds.sniffingMetadataOnly')}
name={['settings', 'reverseSniffing', 'metadataOnly']}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label={t('pages.inbounds.sniffingRouteOnly')}
name={['settings', 'reverseSniffing', 'routeOnly']}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label={t('pages.inbounds.sniffingIpsExcluded')}
name={['settings', 'reverseSniffing', 'ipsExcluded']}
>
<Select
mode="tags"
tokenSeparators={[',']}
placeholder="IP/CIDR/geoip:*"
/>
</Form.Item>
<Form.Item
label={t('pages.inbounds.sniffingDomainsExcluded')}
name={['settings', 'reverseSniffing', 'domainsExcluded']}
>
<Select
mode="tags"
tokenSeparators={[',']}
placeholder="domain:*"
/>
</Form.Item>
</>
)}
</>
<SniffingFields
name={['settings', 'reverseSniffing']}
form={form}
enableLabel={t('pages.xray.outboundForm.reverseSniffing')}
/>
);
}}
</Form.Item>
@@ -1,11 +1,23 @@
import { useTranslation } from 'react-i18next';
import { Form, Input } from 'antd';
import SniffingFields from '@/lib/xray/forms/SniffingFields';
export default function LoopbackFields() {
const { t } = useTranslation();
const form = Form.useFormInstance();
return (
<Form.Item label={t('pages.xray.outboundForm.inboundTag')} name={['settings', 'inboundTag']}>
<Input placeholder={t('pages.xray.outboundForm.inboundTagPlaceholder')} />
</Form.Item>
<>
<Form.Item label={t('pages.xray.outboundForm.inboundTag')} name={['settings', 'inboundTag']}>
<Input placeholder={t('pages.xray.outboundForm.inboundTagPlaceholder')} />
</Form.Item>
<SniffingFields
name={['settings', 'sniffing']}
form={form}
enableLabel={t('pages.inbounds.sniffingTab')}
/>
</>
);
}