feat(frontend): OutboundFormModal.new.tsx DNS + Freedom + VLESS reverse-sniffing

- DNS: rewriteNetwork (udp/tcp Select) + rewriteAddress + rewritePort +
  userLevel + rules Form.List (action/qtype/domain).

- Freedom: domainStrategy + redirect + Fragment Switch with conditional
  4-field sub-block (legacy 'enable Fragment' UX preserved — Switch sets
  all four fields to populated defaults, off-state empties them all out
  so the adapter strips them on submit) + Noises Form.List (rand/base64/
  str/hex types, packet/delay/applyTo per row) + Final Rules Form.List
  with conditional block-delay sub-field.

- VLESS reverse-sniffing slice: rendered only when reverseTag is set
  (matches the legacy modal's nested conditional). All six fields wired
  to the form state with appropriate widgets (Switch / Select multi /
  Select tags).
This commit is contained in:
MHSanaei
2026-05-26 12:08:35 +02:00
parent b6d996d1b1
commit e8721a207c

View File

@@ -30,7 +30,10 @@ import {
type OutboundFormValues,
} from '@/schemas/forms/outbound-form';
import {
DNSRuleActions,
OutboundDomainStrategies,
OutboundProtocols as Protocols,
SNIFFING_OPTION,
TLS_FLOW_CONTROL,
USERS_SECURITY,
WireguardDomainStrategy,
@@ -366,6 +369,389 @@ export default function OutboundFormModalNew({
</Form.Item>
)}
{protocol === 'dns' && (
<>
<Form.Item label="Rewrite network" name={['settings', 'rewriteNetwork']}>
<Select
allowClear
placeholder="(unchanged)"
options={[
{ value: 'udp', label: 'udp' },
{ value: 'tcp', label: 'tcp' },
]}
/>
</Form.Item>
<Form.Item label="Rewrite address" name={['settings', 'rewriteAddress']}>
<Input placeholder="(unchanged) e.g. 1.1.1.1" />
</Form.Item>
<Form.Item label="Rewrite port" name={['settings', 'rewritePort']}>
<InputNumber min={0} max={65535} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="User level" name={['settings', 'userLevel']}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.List name={['settings', 'rules']}>
{(fields, { add, remove }) => (
<>
<Form.Item label="Rules">
<Button
size="small"
type="primary"
icon={<PlusOutlined />}
onClick={() => add({ action: 'direct', qtype: '', domain: '' })}
/>
</Form.Item>
{fields.map((field, index) => (
<div key={field.key}>
<Form.Item wrapperCol={{ md: { span: 14, offset: 8 } }}>
<div className="item-heading">
<span>Rule {index + 1}</span>
<DeleteOutlined
className="danger-icon"
onClick={() => remove(field.name)}
/>
</div>
</Form.Item>
<Form.Item label="Action" name={[field.name, 'action']}>
<Select
options={DNSRuleActions.map((a) => ({ value: a, label: a }))}
/>
</Form.Item>
<Form.Item label="QType" name={[field.name, 'qtype']}>
<Input placeholder="1,3,23-24" />
</Form.Item>
<Form.Item label={t('domainName')} name={[field.name, 'domain']}>
<Input placeholder="domain:example.com" />
</Form.Item>
</div>
))}
</>
)}
</Form.List>
</>
)}
{protocol === 'freedom' && (
<>
<Form.Item label="Strategy" name={['settings', 'domainStrategy']}>
<Select
options={[
{ value: '', label: `(${t('none')})` },
...OutboundDomainStrategies.map((s) => ({ value: s, label: s })),
]}
/>
</Form.Item>
<Form.Item label="Redirect" name={['settings', 'redirect']}>
<Input />
</Form.Item>
<Form.Item label="Fragment" shouldUpdate noStyle>
{() => {
const fragment = (form.getFieldValue(['settings', 'fragment']) ?? {}) as {
packets?: string;
length?: string;
interval?: string;
maxSplit?: string;
};
const enabled = !!(fragment.length || fragment.interval || fragment.maxSplit);
return (
<>
<Form.Item label="Fragment">
<Switch
checked={enabled}
onChange={(checked) => {
form.setFieldValue(
['settings', 'fragment'],
checked
? {
packets: 'tlshello',
length: '100-200',
interval: '10-20',
maxSplit: '300-400',
}
: { packets: '', length: '', interval: '', maxSplit: '' },
);
}}
/>
</Form.Item>
{enabled && (
<>
<Form.Item
label="Packets"
name={['settings', 'fragment', 'packets']}
>
<Select
options={[
{ value: '1-3', label: '1-3' },
{ value: 'tlshello', label: 'tlshello' },
]}
/>
</Form.Item>
<Form.Item label="Length" name={['settings', 'fragment', 'length']}>
<Input />
</Form.Item>
<Form.Item
label="Interval"
name={['settings', 'fragment', 'interval']}
>
<Input />
</Form.Item>
<Form.Item
label="Max Split"
name={['settings', 'fragment', 'maxSplit']}
>
<Input />
</Form.Item>
</>
)}
</>
);
}}
</Form.Item>
<Form.List name={['settings', 'noises']}>
{(fields, { add, remove }) => (
<>
<Form.Item label="Noises">
<Switch
checked={fields.length > 0}
onChange={(checked) => {
if (checked) {
add({
type: 'rand',
packet: '10-20',
delay: '10-16',
applyTo: 'ip',
});
} else {
// remove() with no arg is not supported;
// walk fields in reverse and drop each.
for (let i = fields.length - 1; i >= 0; i--) {
remove(fields[i].name);
}
}
}}
/>
{fields.length > 0 && (
<Button
size="small"
type="primary"
className="ml-8"
icon={<PlusOutlined />}
onClick={() =>
add({
type: 'rand',
packet: '10-20',
delay: '10-16',
applyTo: 'ip',
})
}
/>
)}
</Form.Item>
{fields.map((field, index) => (
<div key={field.key}>
<Form.Item wrapperCol={{ md: { span: 14, offset: 8 } }}>
<div className="item-heading">
<span>Noise {index + 1}</span>
{fields.length > 1 && (
<DeleteOutlined
className="danger-icon"
onClick={() => remove(field.name)}
/>
)}
</div>
</Form.Item>
<Form.Item label="Type" name={[field.name, 'type']}>
<Select
options={['rand', 'base64', 'str', 'hex'].map((v) => ({
value: v,
label: v,
}))}
/>
</Form.Item>
<Form.Item label="Packet" name={[field.name, 'packet']}>
<Input />
</Form.Item>
<Form.Item label="Delay (ms)" name={[field.name, 'delay']}>
<Input />
</Form.Item>
<Form.Item label="Apply to" name={[field.name, 'applyTo']}>
<Select
options={['ip', 'ipv4', 'ipv6'].map((v) => ({
value: v,
label: v,
}))}
/>
</Form.Item>
</div>
))}
</>
)}
</Form.List>
<Form.List name={['settings', 'finalRules']}>
{(fields, { add, remove }) => (
<>
<Form.Item label="Final Rules">
<Button
size="small"
type="primary"
icon={<PlusOutlined />}
onClick={() =>
add({
action: 'allow',
network: '',
port: '',
ip: [],
blockDelay: '',
})
}
/>
<span className="ml-8" style={{ opacity: 0.6 }}>
Override Xray&apos;s default private-IP block
</span>
</Form.Item>
{fields.map((field, index) => (
<div key={field.key}>
<Form.Item wrapperCol={{ md: { span: 14, offset: 8 } }}>
<div className="item-heading">
<span>Rule {index + 1}</span>
<DeleteOutlined
className="danger-icon"
onClick={() => remove(field.name)}
/>
</div>
</Form.Item>
<Form.Item label="Action" name={[field.name, 'action']}>
<Select
options={['allow', 'block'].map((v) => ({
value: v,
label: v,
}))}
/>
</Form.Item>
<Form.Item label="Network" name={[field.name, 'network']}>
<Select
allowClear
placeholder="(any)"
options={['tcp', 'udp', 'tcp,udp'].map((v) => ({
value: v,
label: v,
}))}
/>
</Form.Item>
<Form.Item label="Port" name={[field.name, 'port']}>
<Input placeholder="e.g. 80,443 or 1000-2000" />
</Form.Item>
<Form.Item label="IP / CIDR / geoip" name={[field.name, 'ip']}>
<Select
mode="tags"
tokenSeparators={[',', ' ']}
placeholder="e.g. 10.0.0.0/8, geoip:private"
/>
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
const ruleAction = form.getFieldValue([
'settings',
'finalRules',
field.name,
'action',
]);
if (ruleAction !== 'block') return null;
return (
<Form.Item
label="Block delay (ms)"
name={[field.name, 'blockDelay']}
>
<Input placeholder="optional: 5000-10000" />
</Form.Item>
);
}}
</Form.Item>
</div>
))}
</>
)}
</Form.List>
</>
)}
{protocol === 'vless' && (
<Form.Item shouldUpdate noStyle>
{() => {
const reverseTag = form.getFieldValue(['settings', 'reverseTag']);
if (!reverseTag) return null;
const sniff = (form.getFieldValue(['settings', 'reverseSniffing']) ?? {}) as {
enabled?: boolean;
};
return (
<>
<Form.Item
label="Reverse Sniffing"
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="Metadata Only"
name={['settings', 'reverseSniffing', 'metadataOnly']}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Route Only"
name={['settings', 'reverseSniffing', 'routeOnly']}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="IPs Excluded"
name={['settings', 'reverseSniffing', 'ipsExcluded']}
>
<Select
mode="tags"
tokenSeparators={[',']}
placeholder="IP/CIDR/geoip:*"
/>
</Form.Item>
<Form.Item
label="Domains Excluded"
name={['settings', 'reverseSniffing', 'domainsExcluded']}
>
<Select
mode="tags"
tokenSeparators={[',']}
placeholder="domain:*"
/>
</Form.Item>
</>
)}
</>
);
}}
</Form.Item>
)}
{protocol === 'wireguard' && (
<>
<Form.Item label={t('pages.inbounds.address')} name={['settings', 'address']}>