| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738 |
- import { useMemo } from 'react';
- import { Button, Divider, Form, Input, InputNumber, Select, Switch } from 'antd';
- import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
- import { RandomUtil } from '@/utils';
- import { Protocols } from '@/models/outbound';
- interface StreamShape {
- network?: string;
- kcp?: { mtu?: number };
- finalmask: {
- tcp?: MaskRow[];
- udp?: MaskRow[];
- enableQuicParams?: boolean;
- quicParams?: QuicParams;
- };
- addTcpMask: (type?: string) => void;
- delTcpMask: (index: number) => void;
- addUdpMask: (type?: string) => void;
- delUdpMask: (index: number) => void;
- }
- interface MaskRow {
- type: string;
- settings: Record<string, unknown>;
- _getDefaultSettings: (type: string, settings: Record<string, unknown>) => Record<string, unknown>;
- }
- interface ItemRow {
- type: string;
- packet: string | unknown[];
- delay?: number | string;
- rand?: number | string;
- randRange?: string;
- }
- interface QuicParams {
- congestion: string;
- debug?: boolean;
- brutalUp?: number | string;
- brutalDown?: number | string;
- hasUdpHop?: boolean;
- udpHop?: { ports: string; interval: string | number };
- maxIdleTimeout?: number;
- keepAlivePeriod?: number;
- disablePathMTUDiscovery?: boolean;
- maxIncomingStreams?: number;
- initStreamReceiveWindow?: number;
- maxStreamReceiveWindow?: number;
- initConnectionReceiveWindow?: number;
- maxConnectionReceiveWindow?: number;
- }
- interface FinalMaskFormProps {
- stream: StreamShape;
- protocol: string;
- onChange: () => void;
- }
- function changeMaskType(mask: MaskRow, type: string) {
- mask.type = type;
- mask.settings = mask._getDefaultSettings(type, {});
- }
- function changeItemType(item: ItemRow, type: string) {
- item.type = type;
- if (type === 'base64') item.packet = RandomUtil.randomBase64();
- else if (type === 'array') {
- item.rand = 0;
- item.packet = [];
- } else item.packet = '';
- }
- function newClientServerItem(): ItemRow {
- return { delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: [] };
- }
- function newUdpClientServerItem(): ItemRow {
- return { rand: 0, randRange: '0-255', type: 'array', packet: [] };
- }
- function newNoiseItem(): ItemRow {
- return { rand: '1-8192', randRange: '0-255', type: 'array', packet: [], delay: '10-20' };
- }
- export default function FinalMaskForm({ stream, protocol, onChange }: FinalMaskFormProps) {
- const isHysteria = protocol === Protocols.Hysteria || protocol === 'hysteria';
- const network = stream?.network || '';
- const showTcp = useMemo(
- () => ['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp'].includes(network),
- [network],
- );
- const showUdp = isHysteria || network === 'kcp';
- const showQuic = isHysteria || network === 'xhttp';
- function notify() {
- onChange();
- }
- function changeUdpMaskType(mask: MaskRow, type: string) {
- changeMaskType(mask, type);
- if (network === 'kcp' && stream.kcp) {
- stream.kcp.mtu = type === 'xdns' ? 900 : 1350;
- }
- notify();
- }
- function addUdpMaskWithDefault() {
- const def = isHysteria ? 'salamander' : 'mkcp-aes128gcm';
- stream.addUdpMask(def);
- notify();
- }
- const tcpMasks = stream.finalmask.tcp || [];
- const udpMasks = stream.finalmask.udp || [];
- if (!showTcp && !showUdp && !showQuic) return null;
- return (
- <Form colon={false} labelCol={{ md: { span: 8 } }} wrapperCol={{ md: { span: 14 } }}>
- {showTcp && (
- <>
- <Form.Item label="TCP Masks">
- <Button
- type="primary"
- size="small"
- icon={<PlusOutlined />}
- onClick={() => {
- stream.addTcpMask('fragment');
- notify();
- }}
- />
- </Form.Item>
- {tcpMasks.map((mask, mIdx) => (
- <div key={`tcp-${mIdx}`}>
- <Divider style={{ margin: 0 }}>
- TCP Mask {mIdx + 1}
- <DeleteOutlined
- className="danger-icon"
- onClick={() => {
- stream.delTcpMask(mIdx);
- notify();
- }}
- />
- </Divider>
- <Form.Item label="Type">
- <Select
- value={mask.type}
- onChange={(v) => {
- changeMaskType(mask, v);
- notify();
- }}
- options={[
- { value: 'fragment', label: 'Fragment' },
- { value: 'header-custom', label: 'Header Custom' },
- { value: 'sudoku', label: 'Sudoku' },
- ]}
- />
- </Form.Item>
- {mask.type === 'fragment' && (
- <>
- <Form.Item label="Packets">
- <Select
- value={mask.settings.packets as string}
- onChange={(v) => {
- (mask.settings as Record<string, unknown>).packets = v;
- notify();
- }}
- options={[
- { value: 'tlshello', label: 'tlshello' },
- { value: '1-3', label: '1-3' },
- { value: '1-5', label: '1-5' },
- ]}
- />
- </Form.Item>
- {(['length', 'delay', 'maxSplit'] as const).map((field) => (
- <Form.Item key={field} label={field === 'maxSplit' ? 'Max Split' : field.charAt(0).toUpperCase() + field.slice(1)}>
- <Input
- value={(mask.settings[field] as string) || ''}
- onChange={(e) => {
- (mask.settings as Record<string, unknown>)[field] = e.target.value;
- notify();
- }}
- />
- </Form.Item>
- ))}
- </>
- )}
- {mask.type === 'sudoku' && (
- <>
- {(['password', 'ascii', 'customTable', 'customTables'] as const).map((field) => (
- <Form.Item key={field} label={field === 'customTable' ? 'Custom Table' : field === 'customTables' ? 'Custom Tables' : field.charAt(0).toUpperCase() + field.slice(1)}>
- <Input
- value={(mask.settings[field] as string) || ''}
- onChange={(e) => {
- (mask.settings as Record<string, unknown>)[field] = e.target.value;
- notify();
- }}
- />
- </Form.Item>
- ))}
- {(['paddingMin', 'paddingMax'] as const).map((field) => (
- <Form.Item key={field} label={field === 'paddingMin' ? 'Padding Min' : 'Padding Max'}>
- <InputNumber
- value={(mask.settings[field] as number) || 0}
- min={0}
- onChange={(v) => {
- (mask.settings as Record<string, unknown>)[field] = Number(v) || 0;
- notify();
- }}
- />
- </Form.Item>
- ))}
- </>
- )}
- {mask.type === 'header-custom' && (
- <HeaderCustomGroups mask={mask} kind="tcp" onChange={notify} />
- )}
- </div>
- ))}
- </>
- )}
- {showUdp && (
- <>
- <Form.Item label="UDP Masks">
- <Button type="primary" size="small" icon={<PlusOutlined />} onClick={addUdpMaskWithDefault} />
- </Form.Item>
- {udpMasks.map((mask, mIdx) => (
- <div key={`udp-${mIdx}`}>
- <Divider style={{ margin: 0 }}>
- UDP Mask {mIdx + 1}
- <DeleteOutlined
- className="danger-icon"
- onClick={() => {
- stream.delUdpMask(mIdx);
- notify();
- }}
- />
- </Divider>
- <Form.Item label="Type">
- <Select
- value={mask.type}
- onChange={(v) => changeUdpMaskType(mask, v)}
- 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' },
- ]
- }
- />
- </Form.Item>
- {['mkcp-aes128gcm', 'salamander'].includes(mask.type) && (
- <Form.Item label="Password">
- <Input
- value={(mask.settings.password as string) || ''}
- placeholder="Obfuscation password"
- onChange={(e) => {
- (mask.settings as Record<string, unknown>).password = e.target.value;
- notify();
- }}
- />
- </Form.Item>
- )}
- {mask.type === 'header-dns' && (
- <Form.Item label="Domain">
- <Input
- value={(mask.settings.domain as string) || ''}
- placeholder="e.g., www.example.com"
- onChange={(e) => {
- (mask.settings as Record<string, unknown>).domain = e.target.value;
- notify();
- }}
- />
- </Form.Item>
- )}
- {mask.type === 'xdns' && (
- <Form.Item label="Domains">
- <Select
- mode="tags"
- value={(mask.settings.domains as string[]) || []}
- style={{ width: '100%' }}
- tokenSeparators={[',']}
- placeholder="e.g., www.example.com"
- onChange={(v) => {
- (mask.settings as Record<string, unknown>).domains = v;
- notify();
- }}
- />
- </Form.Item>
- )}
- {mask.type === 'noise' && (
- <NoiseItems mask={mask} onChange={notify} />
- )}
- {mask.type === 'header-custom' && (
- <UdpHeaderCustom mask={mask} onChange={notify} />
- )}
- {mask.type === 'xicmp' && (
- <>
- <Form.Item label="IP">
- <Input
- value={(mask.settings.ip as string) || ''}
- placeholder="0.0.0.0"
- onChange={(e) => {
- (mask.settings as Record<string, unknown>).ip = e.target.value;
- notify();
- }}
- />
- </Form.Item>
- <Form.Item label="ID">
- <InputNumber
- value={(mask.settings.id as number) || 0}
- min={0}
- onChange={(v) => {
- (mask.settings as Record<string, unknown>).id = Number(v) || 0;
- notify();
- }}
- />
- </Form.Item>
- </>
- )}
- </div>
- ))}
- </>
- )}
- {showQuic && (
- <>
- <Form.Item label="QUIC Params">
- <Switch
- checked={!!stream.finalmask.enableQuicParams}
- onChange={(v) => {
- stream.finalmask.enableQuicParams = v;
- notify();
- }}
- />
- </Form.Item>
- {stream.finalmask.enableQuicParams && stream.finalmask.quicParams && (
- <QuicParamsForm params={stream.finalmask.quicParams} onChange={notify} />
- )}
- </>
- )}
- </Form>
- );
- }
- function HeaderCustomGroups({
- mask,
- kind: _kind,
- onChange,
- }: {
- mask: MaskRow;
- kind: 'tcp';
- onChange: () => void;
- }) {
- const settings = mask.settings as { clients?: ItemRow[][]; servers?: ItemRow[][] };
- if (!settings.clients) settings.clients = [];
- if (!settings.servers) settings.servers = [];
- return (
- <>
- {(['clients', 'servers'] as const).map((groupKey) => (
- <div key={groupKey}>
- <Form.Item label={groupKey === 'clients' ? 'Clients' : 'Servers'}>
- <Button
- type="primary"
- size="small"
- icon={<PlusOutlined />}
- onClick={() => {
- (settings[groupKey] as ItemRow[][]).push([newClientServerItem()]);
- onChange();
- }}
- />
- </Form.Item>
- {(settings[groupKey] as ItemRow[][]).map((group, gi) => (
- <div key={`${groupKey}-${gi}`}>
- <Divider style={{ margin: 0 }}>
- {groupKey === 'clients' ? 'Clients' : 'Servers'} Group {gi + 1}
- <DeleteOutlined
- className="danger-icon"
- onClick={() => {
- (settings[groupKey] as ItemRow[][]).splice(gi, 1);
- onChange();
- }}
- />
- </Divider>
- {group.map((item, _ii) => (
- <ItemEditor key={_ii} item={item} onChange={onChange} delayAsNumber />
- ))}
- </div>
- ))}
- </div>
- ))}
- </>
- );
- }
- function UdpHeaderCustom({ mask, onChange }: { mask: MaskRow; onChange: () => void }) {
- const settings = mask.settings as { client?: ItemRow[]; server?: ItemRow[] };
- if (!settings.client) settings.client = [];
- if (!settings.server) settings.server = [];
- return (
- <>
- {(['client', 'server'] as const).map((groupKey) => (
- <div key={groupKey}>
- <Form.Item label={groupKey === 'client' ? 'Client' : 'Server'}>
- <Button
- type="primary"
- size="small"
- icon={<PlusOutlined />}
- onClick={() => {
- (settings[groupKey] as ItemRow[]).push(newUdpClientServerItem());
- onChange();
- }}
- />
- </Form.Item>
- {(settings[groupKey] as ItemRow[]).map((item, ci) => (
- <div key={ci}>
- <Divider style={{ margin: 0 }}>
- {groupKey === 'client' ? 'Client' : 'Server'} {ci + 1}
- <DeleteOutlined
- className="danger-icon"
- onClick={() => {
- (settings[groupKey] as ItemRow[]).splice(ci, 1);
- onChange();
- }}
- />
- </Divider>
- <ItemEditor item={item} onChange={onChange} />
- </div>
- ))}
- </div>
- ))}
- </>
- );
- }
- function NoiseItems({ mask, onChange }: { mask: MaskRow; onChange: () => void }) {
- const settings = mask.settings as { reset?: number; noise?: ItemRow[] };
- if (!settings.noise) settings.noise = [];
- return (
- <>
- <Form.Item label="Reset">
- <InputNumber
- value={settings.reset || 0}
- min={0}
- onChange={(v) => {
- settings.reset = Number(v) || 0;
- onChange();
- }}
- />
- </Form.Item>
- <Form.Item label="Noise">
- <Button
- type="primary"
- size="small"
- icon={<PlusOutlined />}
- onClick={() => {
- (settings.noise as ItemRow[]).push(newNoiseItem());
- onChange();
- }}
- />
- </Form.Item>
- {(settings.noise as ItemRow[]).map((n, ni) => (
- <div key={ni}>
- <Divider style={{ margin: 0 }}>
- Noise {ni + 1}
- <DeleteOutlined
- className="danger-icon"
- onClick={() => {
- (settings.noise as ItemRow[]).splice(ni, 1);
- onChange();
- }}
- />
- </Divider>
- <ItemEditor item={n} onChange={onChange} delayAsString />
- </div>
- ))}
- </>
- );
- }
- function ItemEditor({
- item,
- onChange,
- delayAsNumber,
- delayAsString,
- }: {
- item: ItemRow;
- onChange: () => void;
- delayAsNumber?: boolean;
- delayAsString?: boolean;
- }) {
- return (
- <>
- <Form.Item label="Type">
- <Select
- value={item.type}
- onChange={(v) => {
- changeItemType(item, v);
- onChange();
- }}
- options={[
- { value: 'array', label: 'Array' },
- { value: 'str', label: 'String' },
- { value: 'hex', label: 'Hex' },
- { value: 'base64', label: 'Base64' },
- ]}
- />
- </Form.Item>
- {delayAsNumber && (
- <Form.Item label="Delay (ms)">
- <InputNumber
- value={typeof item.delay === 'number' ? item.delay : 0}
- min={0}
- onChange={(v) => {
- item.delay = Number(v) || 0;
- onChange();
- }}
- />
- </Form.Item>
- )}
- {item.type === 'array' ? (
- <>
- <Form.Item label="Rand">
- {delayAsString ? (
- <Input
- value={String(item.rand ?? '')}
- onChange={(e) => {
- item.rand = e.target.value;
- onChange();
- }}
- placeholder="0 or 1-8192"
- />
- ) : (
- <InputNumber
- value={typeof item.rand === 'number' ? item.rand : 0}
- min={0}
- onChange={(v) => {
- item.rand = Number(v) || 0;
- onChange();
- }}
- />
- )}
- </Form.Item>
- <Form.Item label="Rand Range">
- <Input
- value={item.randRange || ''}
- placeholder="0-255"
- onChange={(e) => {
- item.randRange = e.target.value;
- onChange();
- }}
- />
- </Form.Item>
- </>
- ) : (
- <Form.Item label="Packet">
- {item.type === 'base64' ? (
- <Input.Group compact>
- <Input
- value={String(item.packet ?? '')}
- placeholder="binary data"
- style={{ width: 'calc(100% - 32px)' }}
- onChange={(e) => {
- item.packet = e.target.value;
- onChange();
- }}
- />
- <Button
- icon={<ReloadOutlined />}
- onClick={() => {
- item.packet = RandomUtil.randomBase64();
- onChange();
- }}
- />
- </Input.Group>
- ) : (
- <Input
- value={String(item.packet ?? '')}
- placeholder="binary data"
- onChange={(e) => {
- item.packet = e.target.value;
- onChange();
- }}
- />
- )}
- </Form.Item>
- )}
- {delayAsString && (
- <Form.Item label="Delay">
- <Input
- value={typeof item.delay === 'string' ? item.delay : ''}
- placeholder="10-20"
- onChange={(e) => {
- item.delay = e.target.value;
- onChange();
- }}
- />
- </Form.Item>
- )}
- </>
- );
- }
- function QuicParamsForm({ params, onChange }: { params: QuicParams; onChange: () => void }) {
- function update<K extends keyof QuicParams>(key: K, value: QuicParams[K]) {
- params[key] = value;
- onChange();
- }
- return (
- <>
- <Form.Item label="Congestion">
- <Select
- value={params.congestion}
- onChange={(v) => update('congestion', v)}
- 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">
- <Switch checked={!!params.debug} onChange={(v) => update('debug', v)} />
- </Form.Item>
- {['brutal', 'force-brutal'].includes(params.congestion) && (
- <>
- <Form.Item label="Brutal Up">
- <Input
- value={String(params.brutalUp ?? '')}
- placeholder="65537"
- onChange={(e) => update('brutalUp', e.target.value)}
- />
- </Form.Item>
- <Form.Item label="Brutal Down">
- <Input
- value={String(params.brutalDown ?? '')}
- placeholder="65537"
- onChange={(e) => update('brutalDown', e.target.value)}
- />
- </Form.Item>
- </>
- )}
- <Form.Item label="UDP Hop">
- <Switch checked={!!params.hasUdpHop} onChange={(v) => update('hasUdpHop', v)} />
- </Form.Item>
- {params.hasUdpHop && params.udpHop && (
- <>
- <Form.Item label="Hop Ports">
- <Input
- value={params.udpHop.ports || ''}
- placeholder="e.g. 20000-50000"
- onChange={(e) => {
- params.udpHop!.ports = e.target.value;
- onChange();
- }}
- />
- </Form.Item>
- <Form.Item label="Hop Interval (s)">
- <InputNumber
- value={Number(params.udpHop.interval) || 5}
- min={5}
- onChange={(v) => {
- params.udpHop!.interval = Number(v) || 5;
- onChange();
- }}
- />
- </Form.Item>
- </>
- )}
- {(
- [
- ['maxIdleTimeout', 'Max Idle Timeout (s)', 4, 120],
- ['keepAlivePeriod', 'Keep Alive Period (s)', 2, 60],
- ] as const
- ).map(([key, label, min, max]) => (
- <Form.Item key={key} label={label}>
- <InputNumber
- value={params[key] as number}
- min={min}
- max={max}
- onChange={(v) => update(key, Number(v) || min)}
- />
- </Form.Item>
- ))}
- <Form.Item label="Disable Path MTU Dis">
- <Switch checked={!!params.disablePathMTUDiscovery} onChange={(v) => update('disablePathMTUDiscovery', v)} />
- </Form.Item>
- {(
- [
- ['maxIncomingStreams', 'Max Incoming Streams', 8, '1024 = default'],
- ['initStreamReceiveWindow', 'Init Stream Window', 16384, '8388608 = default'],
- ['maxStreamReceiveWindow', 'Max Stream Window', 16384, '8388608 = default'],
- ['initConnectionReceiveWindow', 'Init Conn Window', 16384, '20971520 = default'],
- ['maxConnectionReceiveWindow', 'Max Conn Window', 16384, '20971520 = default'],
- ] as const
- ).map(([key, label, min, placeholder]) => (
- <Form.Item key={key} label={label}>
- <InputNumber
- value={params[key] as number}
- min={min}
- placeholder={placeholder}
- onChange={(v) => update(key, Number(v) || 0)}
- />
- </Form.Item>
- ))}
- </>
- );
- }
|