mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-30 17:09:34 +00:00
User report: "streamSettings.finalmask.quicParams.udpHop.interval: Invalid input: expected string, received number". Three-part fix: - FinalMaskForm: Hop Interval input changed from InputNumber to Input with "e.g. 5-10" placeholder. xray-core spec says interval is a range string like '5-10' (seconds between min-max hops), not a single number. - FinalMaskForm: defaultQuicParams() seeds interval: '5-10' instead of the broken `interval: 5`. - QuicUdpHopSchema: preprocess coerces number → string for legacy DB rows that were written by the now-fixed buggy UI. Stops the load-time validation crash on existing inbounds. Tests still 296/296.
743 lines
25 KiB
TypeScript
743 lines
25 KiB
TypeScript
import { Button, Divider, Form, Input, InputNumber, Select, Switch } from 'antd';
|
|
import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
|
|
import type { FormInstance } from 'antd/es/form';
|
|
import type { NamePath } from 'antd/es/form/interface';
|
|
|
|
import { RandomUtil } from '@/utils';
|
|
import { OutboundProtocols } from '@/schemas/primitives';
|
|
|
|
// Pattern A FinalMaskForm. Renders a Fragment of Form.Items at absolute
|
|
// paths under `name`; the parent modal owns the Form instance.
|
|
//
|
|
// Naming convention inside Form.List: AntD prefixes Form.Item `name`
|
|
// with the Form.List's own `name`. So Form.Items inside the render
|
|
// prop use RELATIVE paths (e.g. `[field.name, 'type']`). Nested
|
|
// Form.Lists also use relative names. Using absolute paths here would
|
|
// double up the prefix and silently route reads/writes to the wrong
|
|
// storage path.
|
|
|
|
export interface FinalMaskFormProps {
|
|
name: NamePath;
|
|
network: string;
|
|
protocol: string;
|
|
form: FormInstance;
|
|
}
|
|
|
|
const TCP_NETWORKS = ['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp'];
|
|
|
|
function asPath(name: NamePath): (string | number)[] {
|
|
return Array.isArray(name) ? [...name] : [name];
|
|
}
|
|
|
|
function defaultTcpMaskSettings(type: string): Record<string, unknown> {
|
|
switch (type) {
|
|
case 'fragment':
|
|
return { packets: '1-3', length: '', delay: '', maxSplit: '' };
|
|
case 'sudoku':
|
|
return {
|
|
password: '', ascii: '', customTable: '', customTables: '',
|
|
paddingMin: 0, paddingMax: 0,
|
|
};
|
|
case 'header-custom':
|
|
return { clients: [], servers: [] };
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function defaultUdpMaskSettings(type: string): Record<string, unknown> {
|
|
switch (type) {
|
|
case 'salamander':
|
|
case 'mkcp-aes128gcm':
|
|
return { password: '' };
|
|
case 'header-dns':
|
|
return { domain: '' };
|
|
case 'xdns':
|
|
return { domains: [] };
|
|
case 'xicmp':
|
|
return { ip: '0.0.0.0', id: 0 };
|
|
case 'header-custom':
|
|
return { client: [], server: [] };
|
|
case 'noise':
|
|
return { reset: 0, noise: [] };
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function defaultClientServerItem(): Record<string, unknown> {
|
|
return { delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: [] };
|
|
}
|
|
|
|
function defaultUdpClientServerItem(): Record<string, unknown> {
|
|
return { rand: 0, randRange: '0-255', type: 'array', packet: [] };
|
|
}
|
|
|
|
function defaultNoiseItem(): Record<string, unknown> {
|
|
return {
|
|
rand: '1-8192', randRange: '0-255', type: 'array', packet: [], delay: '10-20',
|
|
};
|
|
}
|
|
|
|
function defaultQuicParams(): Record<string, unknown> {
|
|
// Seeded with the xray-core / hysteria recommended defaults so the QUIC
|
|
// Params sub-form doesn't show blank InputNumber fields when first
|
|
// enabled. The schema declares these as .optional() (no Zod default)
|
|
// because the wire shape omits them when xray's built-in default
|
|
// applies — but the panel needs values to render the controls.
|
|
return {
|
|
congestion: 'bbr',
|
|
debug: false,
|
|
brutalUp: 0,
|
|
brutalDown: 0,
|
|
hasUdpHop: false,
|
|
udpHop: { ports: '20000-50000', interval: '5-10' },
|
|
maxIdleTimeout: 30,
|
|
keepAlivePeriod: 10,
|
|
disablePathMTUDiscovery: false,
|
|
maxIncomingStreams: 1024,
|
|
initStreamReceiveWindow: 8388608,
|
|
maxStreamReceiveWindow: 8388608,
|
|
initConnectionReceiveWindow: 20971520,
|
|
maxConnectionReceiveWindow: 20971520,
|
|
};
|
|
}
|
|
|
|
export default function FinalMaskForm({ name, network, protocol, form }: FinalMaskFormProps) {
|
|
const base = asPath(name);
|
|
const isHysteria = protocol === OutboundProtocols.Hysteria || protocol === 'hysteria';
|
|
const showTcp = TCP_NETWORKS.includes(network);
|
|
const showUdp = isHysteria || network === 'kcp';
|
|
const showQuic = isHysteria || network === 'xhttp';
|
|
const enableQuic = Form.useWatch([...base, 'enableQuicParams'], form);
|
|
|
|
if (!showTcp && !showUdp && !showQuic) return null;
|
|
|
|
return (
|
|
<>
|
|
{showTcp && <TcpMasksList base={base} form={form} />}
|
|
{showUdp && <UdpMasksList base={base} form={form} isHysteria={isHysteria} network={network} />}
|
|
{showQuic && (
|
|
<>
|
|
<Form.Item label="QUIC Params" name={[...base, 'enableQuicParams']} valuePropName="checked">
|
|
<Switch
|
|
onChange={(v) => {
|
|
if (v) {
|
|
const current = form.getFieldValue([...base, 'quicParams']);
|
|
if (!current) form.setFieldValue([...base, 'quicParams'], defaultQuicParams());
|
|
}
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
{enableQuic && <QuicParamsForm base={[...base, 'quicParams']} form={form} />}
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function TcpMasksList({ base, form }: { base: (string | number)[]; form: FormInstance }) {
|
|
return (
|
|
<Form.List name={[...base, 'tcp']}>
|
|
{(fields, { add, remove }) => (
|
|
<>
|
|
<Form.Item label="TCP Masks">
|
|
<Button
|
|
type="primary"
|
|
size="small"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => add({ type: 'fragment', settings: defaultTcpMaskSettings('fragment') })}
|
|
/>
|
|
</Form.Item>
|
|
{fields.map((field, mIdx) => (
|
|
<TcpMaskItem
|
|
key={field.key}
|
|
fieldName={field.name}
|
|
displayIndex={mIdx + 1}
|
|
form={form}
|
|
listPath={[...base, 'tcp']}
|
|
onRemove={() => remove(field.name)}
|
|
/>
|
|
))}
|
|
</>
|
|
)}
|
|
</Form.List>
|
|
);
|
|
}
|
|
|
|
function TcpMaskItem({
|
|
fieldName, displayIndex, form, listPath, onRemove,
|
|
}: {
|
|
fieldName: number;
|
|
displayIndex: number;
|
|
form: FormInstance;
|
|
listPath: (string | number)[];
|
|
onRemove: () => void;
|
|
}) {
|
|
// Absolute path for setFieldValue side effects (resetting settings on
|
|
// type change). All Form.Item `name=` use RELATIVE paths within the
|
|
// outer Form.List context.
|
|
const absolutePath = [...listPath, fieldName];
|
|
|
|
return (
|
|
<div>
|
|
<Divider style={{ margin: 0 }}>
|
|
TCP Mask {displayIndex}
|
|
<DeleteOutlined className="danger-icon" onClick={onRemove} />
|
|
</Divider>
|
|
|
|
<Form.Item label="Type" name={[fieldName, 'type']}>
|
|
<Select
|
|
onChange={(v) =>
|
|
form.setFieldValue([...absolutePath, 'settings'], defaultTcpMaskSettings(v))
|
|
}
|
|
options={[
|
|
{ value: 'fragment', label: 'Fragment' },
|
|
{ value: 'header-custom', label: 'Header Custom' },
|
|
{ value: 'sudoku', label: 'Sudoku' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
noStyle
|
|
shouldUpdate={(prev, curr) => {
|
|
const a = getDeep(prev, [...absolutePath, 'type']);
|
|
const b = getDeep(curr, [...absolutePath, 'type']);
|
|
return a !== b;
|
|
}}
|
|
>
|
|
{({ getFieldValue }) => {
|
|
const type = getFieldValue([...absolutePath, 'type']) as string | undefined;
|
|
if (type === 'fragment') {
|
|
return (
|
|
<>
|
|
<Form.Item label="Packets" name={[fieldName, 'settings', 'packets']}>
|
|
<Select
|
|
options={[
|
|
{ value: 'tlshello', label: 'tlshello' },
|
|
{ value: '1-3', label: '1-3' },
|
|
{ value: '1-5', label: '1-5' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item label="Length" name={[fieldName, 'settings', 'length']}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item label="Delay" name={[fieldName, 'settings', 'delay']}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item label="Max Split" name={[fieldName, 'settings', 'maxSplit']}>
|
|
<Input />
|
|
</Form.Item>
|
|
</>
|
|
);
|
|
}
|
|
if (type === 'sudoku') {
|
|
return (
|
|
<>
|
|
<Form.Item label="Password" name={[fieldName, 'settings', 'password']}><Input /></Form.Item>
|
|
<Form.Item label="ASCII" name={[fieldName, 'settings', 'ascii']}><Input /></Form.Item>
|
|
<Form.Item label="Custom Table" name={[fieldName, 'settings', 'customTable']}><Input /></Form.Item>
|
|
<Form.Item label="Custom Tables" name={[fieldName, 'settings', 'customTables']}><Input /></Form.Item>
|
|
<Form.Item label="Padding Min" name={[fieldName, 'settings', 'paddingMin']}>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item label="Padding Max" name={[fieldName, 'settings', 'paddingMax']}>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
</>
|
|
);
|
|
}
|
|
if (type === 'header-custom') {
|
|
return (
|
|
<HeaderCustomGroups
|
|
tcpFieldName={fieldName}
|
|
form={form}
|
|
absoluteSettingsPath={[...absolutePath, 'settings']}
|
|
/>
|
|
);
|
|
}
|
|
return null;
|
|
}}
|
|
</Form.Item>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 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 getDeep(obj: unknown, path: (string | number)[]): unknown {
|
|
let cur: unknown = obj;
|
|
for (const key of path) {
|
|
if (cur == null || typeof cur !== 'object') return undefined;
|
|
cur = (cur as Record<string | number, unknown>)[key];
|
|
}
|
|
return cur;
|
|
}
|
|
|
|
function HeaderCustomGroups({
|
|
tcpFieldName, form, absoluteSettingsPath,
|
|
}: {
|
|
tcpFieldName: number;
|
|
form: FormInstance;
|
|
absoluteSettingsPath: (string | number)[];
|
|
}) {
|
|
return (
|
|
<>
|
|
{(['clients', 'servers'] as const).map((groupKey) => (
|
|
<Form.List key={groupKey} name={[tcpFieldName, 'settings', groupKey]}>
|
|
{(groups, { add: addGroup, remove: removeGroup }) => (
|
|
<>
|
|
<Form.Item label={groupKey === 'clients' ? 'Clients' : 'Servers'}>
|
|
<Button
|
|
type="primary"
|
|
size="small"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => addGroup([defaultClientServerItem()])}
|
|
/>
|
|
</Form.Item>
|
|
{groups.map((group, gi) => (
|
|
<div key={group.key}>
|
|
<Divider style={{ margin: 0 }}>
|
|
{groupKey === 'clients' ? 'Clients' : 'Servers'} Group {gi + 1}
|
|
<DeleteOutlined className="danger-icon" onClick={() => removeGroup(group.name)} />
|
|
</Divider>
|
|
<Form.List name={[group.name]}>
|
|
{(items, { add: addItem, remove: removeItem }) => (
|
|
<>
|
|
<Form.Item label="Items">
|
|
<Button
|
|
size="small"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => addItem(defaultClientServerItem())}
|
|
/>
|
|
</Form.Item>
|
|
{items.map((item) => (
|
|
<ItemEditor
|
|
key={item.key}
|
|
fieldName={item.name}
|
|
form={form}
|
|
absoluteItemPath={[...absoluteSettingsPath, groupKey, group.name, item.name]}
|
|
delayMode="number"
|
|
onRemove={() => removeItem(item.name)}
|
|
/>
|
|
))}
|
|
</>
|
|
)}
|
|
</Form.List>
|
|
</div>
|
|
))}
|
|
</>
|
|
)}
|
|
</Form.List>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function UdpMasksList({
|
|
base, form, isHysteria, network,
|
|
}: { base: (string | number)[]; form: FormInstance; isHysteria: boolean; network: string }) {
|
|
return (
|
|
<Form.List name={[...base, 'udp']}>
|
|
{(fields, { add, remove }) => (
|
|
<>
|
|
<Form.Item label="UDP Masks">
|
|
<Button
|
|
type="primary"
|
|
size="small"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => {
|
|
const def = isHysteria ? 'salamander' : 'mkcp-aes128gcm';
|
|
add({ type: def, settings: defaultUdpMaskSettings(def) });
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
{fields.map((field, mIdx) => (
|
|
<UdpMaskItem
|
|
key={field.key}
|
|
fieldName={field.name}
|
|
displayIndex={mIdx + 1}
|
|
form={form}
|
|
listPath={[...base, 'udp']}
|
|
isHysteria={isHysteria}
|
|
network={network}
|
|
onRemove={() => remove(field.name)}
|
|
/>
|
|
))}
|
|
</>
|
|
)}
|
|
</Form.List>
|
|
);
|
|
}
|
|
|
|
function UdpMaskItem({
|
|
fieldName, displayIndex, form, listPath, isHysteria, network, onRemove,
|
|
}: {
|
|
fieldName: number;
|
|
displayIndex: number;
|
|
form: FormInstance;
|
|
listPath: (string | number)[];
|
|
isHysteria: boolean;
|
|
network: string;
|
|
onRemove: () => void;
|
|
}) {
|
|
const absolutePath = [...listPath, fieldName];
|
|
|
|
const onTypeChange = (v: string) => {
|
|
form.setFieldValue([...absolutePath, 'settings'], defaultUdpMaskSettings(v));
|
|
if (network === 'kcp') {
|
|
const kcpMtuPath = [...listPath.slice(0, -1), 'kcpSettings', 'mtu'];
|
|
form.setFieldValue(kcpMtuPath, v === 'xdns' ? 900 : 1350);
|
|
}
|
|
};
|
|
|
|
const options = isHysteria
|
|
? [{ value: 'salamander', label: 'Salamander (Hysteria2)' }]
|
|
: [
|
|
{ value: 'mkcp-aes128gcm', label: 'mKCP AES-128-GCM' },
|
|
{ value: 'header-dns', label: 'Header DNS' },
|
|
{ value: 'header-dtls', label: 'Header DTLS 1.2' },
|
|
{ value: 'header-srtp', label: 'Header SRTP' },
|
|
{ value: 'header-utp', label: 'Header uTP' },
|
|
{ value: 'header-wechat', label: 'Header WeChat Video' },
|
|
{ value: 'header-wireguard', label: 'Header WireGuard' },
|
|
{ value: 'mkcp-original', label: 'mKCP Original' },
|
|
{ value: 'xdns', label: 'xDNS' },
|
|
{ value: 'xicmp', label: 'xICMP' },
|
|
{ value: 'header-custom', label: 'Header Custom' },
|
|
{ value: 'noise', label: 'Noise' },
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<Divider style={{ margin: 0 }}>
|
|
UDP Mask {displayIndex}
|
|
<DeleteOutlined className="danger-icon" onClick={onRemove} />
|
|
</Divider>
|
|
|
|
<Form.Item label="Type" name={[fieldName, 'type']}>
|
|
<Select onChange={onTypeChange} options={options} />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
noStyle
|
|
shouldUpdate={(prev, curr) => getDeep(prev, [...absolutePath, 'type']) !== getDeep(curr, [...absolutePath, 'type'])}
|
|
>
|
|
{({ getFieldValue }) => {
|
|
const type = getFieldValue([...absolutePath, 'type']) as string | undefined;
|
|
if (type === 'mkcp-aes128gcm' || type === 'salamander') {
|
|
return (
|
|
<Form.Item label="Password" name={[fieldName, 'settings', 'password']}>
|
|
<Input placeholder="Obfuscation password" />
|
|
</Form.Item>
|
|
);
|
|
}
|
|
if (type === 'header-dns') {
|
|
return (
|
|
<Form.Item label="Domain" name={[fieldName, 'settings', 'domain']}>
|
|
<Input placeholder="e.g., www.example.com" />
|
|
</Form.Item>
|
|
);
|
|
}
|
|
if (type === 'xdns') {
|
|
return (
|
|
<Form.Item label="Domains" name={[fieldName, 'settings', 'domains']}>
|
|
<Select mode="tags" style={{ width: '100%' }} tokenSeparators={[',']} />
|
|
</Form.Item>
|
|
);
|
|
}
|
|
if (type === 'xicmp') {
|
|
return (
|
|
<>
|
|
<Form.Item label="IP" name={[fieldName, 'settings', 'ip']}>
|
|
<Input placeholder="0.0.0.0" />
|
|
</Form.Item>
|
|
<Form.Item label="ID" name={[fieldName, 'settings', 'id']}>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
</>
|
|
);
|
|
}
|
|
if (type === 'header-custom') {
|
|
return (
|
|
<UdpHeaderCustom
|
|
udpFieldName={fieldName}
|
|
form={form}
|
|
absoluteSettingsPath={[...absolutePath, 'settings']}
|
|
/>
|
|
);
|
|
}
|
|
if (type === 'noise') {
|
|
return (
|
|
<NoiseItems
|
|
udpFieldName={fieldName}
|
|
form={form}
|
|
absoluteSettingsPath={[...absolutePath, 'settings']}
|
|
/>
|
|
);
|
|
}
|
|
return null;
|
|
}}
|
|
</Form.Item>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function UdpHeaderCustom({
|
|
udpFieldName, form, absoluteSettingsPath,
|
|
}: {
|
|
udpFieldName: number;
|
|
form: FormInstance;
|
|
absoluteSettingsPath: (string | number)[];
|
|
}) {
|
|
return (
|
|
<>
|
|
{(['client', 'server'] as const).map((groupKey) => (
|
|
<Form.List key={groupKey} name={[udpFieldName, 'settings', groupKey]}>
|
|
{(items, { add, remove }) => (
|
|
<>
|
|
<Form.Item label={groupKey === 'client' ? 'Client' : 'Server'}>
|
|
<Button
|
|
type="primary"
|
|
size="small"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => add(defaultUdpClientServerItem())}
|
|
/>
|
|
</Form.Item>
|
|
{items.map((item, ci) => (
|
|
<div key={item.key}>
|
|
<Divider style={{ margin: 0 }}>
|
|
{groupKey === 'client' ? 'Client' : 'Server'} {ci + 1}
|
|
<DeleteOutlined className="danger-icon" onClick={() => remove(item.name)} />
|
|
</Divider>
|
|
<ItemEditor
|
|
fieldName={item.name}
|
|
form={form}
|
|
absoluteItemPath={[...absoluteSettingsPath, groupKey, item.name]}
|
|
onRemove={() => remove(item.name)}
|
|
/>
|
|
</div>
|
|
))}
|
|
</>
|
|
)}
|
|
</Form.List>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function NoiseItems({
|
|
udpFieldName, form, absoluteSettingsPath,
|
|
}: {
|
|
udpFieldName: number;
|
|
form: FormInstance;
|
|
absoluteSettingsPath: (string | number)[];
|
|
}) {
|
|
return (
|
|
<>
|
|
<Form.Item label="Reset" name={[udpFieldName, 'settings', 'reset']}>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.List name={[udpFieldName, 'settings', 'noise']}>
|
|
{(items, { add, remove }) => (
|
|
<>
|
|
<Form.Item label="Noise">
|
|
<Button
|
|
type="primary"
|
|
size="small"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => add(defaultNoiseItem())}
|
|
/>
|
|
</Form.Item>
|
|
{items.map((item, ni) => (
|
|
<div key={item.key}>
|
|
<Divider style={{ margin: 0 }}>
|
|
Noise {ni + 1}
|
|
<DeleteOutlined className="danger-icon" onClick={() => remove(item.name)} />
|
|
</Divider>
|
|
<ItemEditor
|
|
fieldName={item.name}
|
|
form={form}
|
|
absoluteItemPath={[...absoluteSettingsPath, 'noise', item.name]}
|
|
delayMode="string"
|
|
onRemove={() => remove(item.name)}
|
|
/>
|
|
</div>
|
|
))}
|
|
</>
|
|
)}
|
|
</Form.List>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function ItemEditor({
|
|
fieldName, form, absoluteItemPath, delayMode, onRemove: _onRemove,
|
|
}: {
|
|
fieldName: number;
|
|
form: FormInstance;
|
|
absoluteItemPath: (string | number)[];
|
|
delayMode?: 'number' | 'string';
|
|
onRemove?: () => void;
|
|
}) {
|
|
const onTypeChange = (v: string) => {
|
|
if (v === 'base64') {
|
|
form.setFieldValue([...absoluteItemPath, 'packet'], RandomUtil.randomBase64());
|
|
} else if (v === 'array') {
|
|
form.setFieldValue([...absoluteItemPath, 'rand'], delayMode === 'string' ? '1-8192' : 0);
|
|
form.setFieldValue([...absoluteItemPath, 'packet'], []);
|
|
} else {
|
|
form.setFieldValue([...absoluteItemPath, 'packet'], '');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Form.Item label="Type" name={[fieldName, 'type']}>
|
|
<Select
|
|
onChange={onTypeChange}
|
|
options={[
|
|
{ value: 'array', label: 'Array' },
|
|
{ value: 'str', label: 'String' },
|
|
{ value: 'hex', label: 'Hex' },
|
|
{ value: 'base64', label: 'Base64' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
|
|
{delayMode === 'number' && (
|
|
<Form.Item label="Delay (ms)" name={[fieldName, 'delay']}>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
)}
|
|
{delayMode === 'string' && (
|
|
<Form.Item label="Delay" name={[fieldName, 'delay']}>
|
|
<Input placeholder="10-20" />
|
|
</Form.Item>
|
|
)}
|
|
|
|
<Form.Item
|
|
noStyle
|
|
shouldUpdate={(prev, curr) => getDeep(prev, [...absoluteItemPath, 'type']) !== getDeep(curr, [...absoluteItemPath, 'type'])}
|
|
>
|
|
{({ getFieldValue }) => {
|
|
const type = getFieldValue([...absoluteItemPath, 'type']) as string | undefined;
|
|
if (type === 'array') {
|
|
return (
|
|
<>
|
|
<Form.Item label="Rand" name={[fieldName, 'rand']}>
|
|
{delayMode === 'string' ? (
|
|
<Input placeholder="0 or 1-8192" />
|
|
) : (
|
|
<InputNumber min={0} />
|
|
)}
|
|
</Form.Item>
|
|
<Form.Item label="Rand Range" name={[fieldName, 'randRange']}>
|
|
<Input placeholder="0-255" />
|
|
</Form.Item>
|
|
</>
|
|
);
|
|
}
|
|
if (type === 'base64') {
|
|
return (
|
|
<Form.Item label="Packet">
|
|
<Input.Group compact>
|
|
<Form.Item name={[fieldName, 'packet']} noStyle>
|
|
<Input placeholder="binary data" style={{ width: 'calc(100% - 32px)' }} />
|
|
</Form.Item>
|
|
<Button
|
|
icon={<ReloadOutlined />}
|
|
onClick={() => form.setFieldValue([...absoluteItemPath, 'packet'], RandomUtil.randomBase64())}
|
|
/>
|
|
</Input.Group>
|
|
</Form.Item>
|
|
);
|
|
}
|
|
return (
|
|
<Form.Item label="Packet" name={[fieldName, 'packet']}>
|
|
<Input placeholder="binary data" />
|
|
</Form.Item>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function QuicParamsForm({ base, form }: { base: (string | number)[]; form: FormInstance }) {
|
|
const congestion = Form.useWatch([...base, 'congestion'], form) as string | undefined;
|
|
const hasUdpHop = Form.useWatch([...base, 'hasUdpHop'], form) as boolean | undefined;
|
|
|
|
return (
|
|
<>
|
|
<Form.Item label="Congestion" name={[...base, 'congestion']}>
|
|
<Select
|
|
options={[
|
|
{ value: 'reno', label: 'Reno' },
|
|
{ value: 'bbr', label: 'BBR' },
|
|
{ value: 'brutal', label: 'Brutal' },
|
|
{ value: 'force-brutal', label: 'Force Brutal' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item label="Debug" name={[...base, 'debug']} valuePropName="checked">
|
|
<Switch />
|
|
</Form.Item>
|
|
|
|
{(congestion === 'brutal' || congestion === 'force-brutal') && (
|
|
<>
|
|
<Form.Item label="Brutal Up" name={[...base, 'brutalUp']}>
|
|
<Input placeholder="65537" />
|
|
</Form.Item>
|
|
<Form.Item label="Brutal Down" name={[...base, 'brutalDown']}>
|
|
<Input placeholder="65537" />
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
|
|
<Form.Item label="UDP Hop" name={[...base, 'hasUdpHop']} valuePropName="checked">
|
|
<Switch />
|
|
</Form.Item>
|
|
{hasUdpHop && (
|
|
<>
|
|
<Form.Item label="Hop Ports" name={[...base, 'udpHop', 'ports']}>
|
|
<Input placeholder="e.g. 20000-50000" />
|
|
</Form.Item>
|
|
<Form.Item label="Hop Interval (s)" name={[...base, 'udpHop', 'interval']}>
|
|
<Input placeholder="e.g. 5-10" />
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
|
|
<Form.Item label="Max Idle Timeout (s)" name={[...base, 'maxIdleTimeout']}>
|
|
<InputNumber min={4} max={120} />
|
|
</Form.Item>
|
|
<Form.Item label="Keep Alive Period (s)" name={[...base, 'keepAlivePeriod']}>
|
|
<InputNumber min={2} max={60} />
|
|
</Form.Item>
|
|
<Form.Item label="Disable Path MTU Dis" name={[...base, 'disablePathMTUDiscovery']} valuePropName="checked">
|
|
<Switch />
|
|
</Form.Item>
|
|
|
|
<Form.Item label="Max Incoming Streams" name={[...base, 'maxIncomingStreams']}>
|
|
<InputNumber min={8} placeholder="1024 = default" />
|
|
</Form.Item>
|
|
<Form.Item label="Init Stream Window" name={[...base, 'initStreamReceiveWindow']}>
|
|
<InputNumber min={16384} placeholder="8388608 = default" />
|
|
</Form.Item>
|
|
<Form.Item label="Max Stream Window" name={[...base, 'maxStreamReceiveWindow']}>
|
|
<InputNumber min={16384} placeholder="8388608 = default" />
|
|
</Form.Item>
|
|
<Form.Item label="Init Conn Window" name={[...base, 'initConnectionReceiveWindow']}>
|
|
<InputNumber min={16384} placeholder="20971520 = default" />
|
|
</Form.Item>
|
|
<Form.Item label="Max Conn Window" name={[...base, 'maxConnectionReceiveWindow']}>
|
|
<InputNumber min={16384} placeholder="20971520 = default" />
|
|
</Form.Item>
|
|
</>
|
|
);
|
|
}
|