| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553 |
- import { useEffect, useRef } from 'react';
- import {
- AutoComplete,
- Button,
- Divider,
- Form,
- Input,
- InputNumber,
- Select,
- Space,
- Switch,
- } from 'antd';
- import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
- import { useTranslation } from 'react-i18next';
- import type { FormInstance } from 'antd/es/form';
- import type { NamePath } from 'antd/es/form/interface';
- import { RandomUtil } from '@/utils';
- import { activateOnKey } from '@/utils/a11y';
- import { OutboundProtocols, UTLS_FINGERPRINT } from '@/schemas/primitives';
- const UTLS_FINGERPRINT_OPTIONS = Object.values(UTLS_FINGERPRINT).map((value) => ({
- value,
- label: value,
- }));
- export interface FinalMaskFormProps {
- name: NamePath;
- network: string;
- protocol: string;
- form: FormInstance;
- // When true, all sections (TCP / UDP / QUIC) are shown regardless of
- // network/protocol. Used by the global sub-JSON finalmask editor where
- // the masks apply to every stream rather than one specific transport.
- showAll?: boolean;
- }
- const TCP_NETWORKS = ['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp'];
- const DEFAULT_GECKO_PACKET_SIZE = { min: 512, max: 1200 };
- // Xray-core caps the Gecko output packet size at its internal buffer (2048)
- // and needs 1 <= min <= max; mirror those bounds so the panel rejects what
- // core would reject at runtime (salamander/conn.go).
- const GECKO_MIN_PACKET_SIZE = 1;
- const GECKO_MAX_PACKET_SIZE = 2048;
- export function parseGeckoPacketSize(value: unknown): { min: number; max: number } | null {
- const str = typeof value === 'string' ? value.trim() : String(value ?? '').trim();
- const match = /^(\d+)-(\d+)$/.exec(str);
- if (!match) return null;
- const min = Number(match[1]);
- const max = Number(match[2]);
- if (
- !Number.isSafeInteger(min) ||
- !Number.isSafeInteger(max) ||
- min < GECKO_MIN_PACKET_SIZE ||
- max < min ||
- max > GECKO_MAX_PACKET_SIZE
- ) {
- return null;
- }
- return { min, max };
- }
- function formatGeckoPacketSize(min: number, max: number): string {
- return `${min}-${max}`;
- }
- function splitGeckoPacketSize(value: unknown): { min: number | null; max: number | null } {
- const str = typeof value === 'string' ? value.trim() : String(value ?? '').trim();
- const [minRaw = '', maxRaw = ''] = str.split('-', 2);
- const min = /^\d+$/.test(minRaw) ? Number(minRaw) : null;
- const max = /^\d+$/.test(maxRaw) ? Number(maxRaw) : null;
- return { min, max };
- }
- function validateGeckoPacketSize(_rule: unknown, value: unknown): Promise<void> {
- if (parseGeckoPacketSize(value)) return Promise.resolve();
- return Promise.reject(
- new Error(
- `Use a range like 512-1200 (${GECKO_MIN_PACKET_SIZE}-${GECKO_MAX_PACKET_SIZE}, max ≥ min)`,
- ),
- );
- }
- function asPath(name: NamePath): (string | number)[] {
- return Array.isArray(name) ? [...name] : [name];
- }
- function defaultTcpMaskSettings(type: string): Record<string, unknown> {
- switch (type) {
- case 'fragment':
- // `lengths`/`delays` are per-segment range arrays (xray-core #6334);
- // a single length entry reproduces the legacy single-range behavior.
- return { packets: '1-3', lengths: ['100-200'], delays: [], maxSplit: '' };
- case 'sudoku':
- return {
- password: '',
- ascii: '',
- customTable: '',
- customTables: [],
- paddingMin: 0,
- paddingMax: 0,
- };
- case 'header-custom':
- return { clients: [], servers: [] };
- case 'xmc':
- return {
- hostname: '',
- profiles: [defaultXmcProfile()],
- password: RandomUtil.randomLowerAndNum(16),
- };
- default:
- return {};
- }
- }
- function defaultXmcProfile(): Record<string, unknown> {
- return { username: '', uuid: '', texturesValue: '', texturesSignature: '' };
- }
- // xray-core #6487 replaced the xmc mask's `usernames` string list with
- // `profiles` objects carrying a Mojang-signed session profile, and dropped the
- // "default to Dream" fallback so at least one complete profile is now
- // mandatory. The signature can only come from Mojang's session server, so a
- // legacy username cannot be upgraded automatically — carry it into a profile
- // stub instead, which keeps the operator's player names visible and leaves the
- // per-field validators pointing at exactly what still has to be filled in.
- export function migrateXmcSettings(settings: Record<string, unknown>): {
- next: Record<string, unknown>;
- changed: boolean;
- } {
- const out: Record<string, unknown> = { ...settings };
- let changed = false;
- if (!Array.isArray(out.profiles) && Array.isArray(out.usernames)) {
- out.profiles = out.usernames
- .filter((name): name is string => typeof name === 'string' && name.trim() !== '')
- .map((name) => ({ ...defaultXmcProfile(), username: name }));
- changed = true;
- }
- if ('usernames' in out) {
- delete out.usernames;
- changed = true;
- }
- if (!Array.isArray(out.profiles)) {
- out.profiles = [];
- changed = true;
- }
- return { next: out, changed };
- }
- // xray-core #6334 replaced a fragment mask's single `length`/`delay` ranges
- // with `lengths`/`delays` arrays (the singular keys remain in core only as a
- // fallback). Lift any legacy singular value into a one-element array so the
- // list UI shows it, and drop the singular key so we never emit both.
- function migrateFragmentSettings(settings: Record<string, unknown>): {
- next: Record<string, unknown>;
- changed: boolean;
- } {
- const out: Record<string, unknown> = { ...settings };
- let changed = false;
- if (!Array.isArray(out.lengths) && typeof out.length === 'string' && out.length.trim() !== '') {
- out.lengths = [out.length];
- changed = true;
- }
- if ('length' in out) {
- delete out.length;
- changed = true;
- }
- if (!Array.isArray(out.delays) && typeof out.delay === 'string' && out.delay.trim() !== '') {
- out.delays = [out.delay];
- changed = true;
- }
- if ('delay' in out) {
- delete out.delay;
- changed = true;
- }
- return { next: out, changed };
- }
- function defaultUdpMaskSettings(type: string): Record<string, unknown> {
- switch (type) {
- case 'salamander':
- return { password: '' };
- case 'mkcp-legacy':
- return { header: '', value: '' };
- case 'xdns':
- return { domains: [] };
- case 'xicmp':
- return { dgram: false, ips: [] };
- case 'realm':
- return { url: '', stunServers: [] };
- 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> {
- return {
- congestion: 'bbr',
- debug: false,
- maxIdleTimeout: 30,
- keepAlivePeriod: 10,
- disablePathMTUDiscovery: false,
- maxIncomingStreams: 1024,
- initStreamReceiveWindow: 8388608,
- maxStreamReceiveWindow: 8388608,
- initConnectionReceiveWindow: 20971520,
- maxConnectionReceiveWindow: 20971520,
- };
- }
- function defaultUdpHop(): Record<string, unknown> {
- return { ports: '20000-50000', interval: '5-10' };
- }
- export default function FinalMaskForm({
- name,
- network,
- protocol,
- form,
- showAll = false,
- }: FinalMaskFormProps) {
- const base = asPath(name);
- // Migrate legacy TCP mask shapes once on mount so configs saved before
- // #6334 (fragment ranges) and #6487 (xmc profiles) render in the list UI.
- const migratedRef = useRef(false);
- useEffect(() => {
- if (migratedRef.current) return;
- migratedRef.current = true;
- const tcp = form.getFieldValue([...base, 'tcp']);
- if (!Array.isArray(tcp)) return;
- let anyChanged = false;
- const next = tcp.map((mask) => {
- if (!mask || typeof mask !== 'object') return mask;
- const m = mask as Record<string, unknown>;
- if (m.type !== 'fragment' && m.type !== 'xmc') return mask;
- if (!m.settings || typeof m.settings !== 'object') return mask;
- const settings = m.settings as Record<string, unknown>;
- const { next: migrated, changed } =
- m.type === 'fragment' ? migrateFragmentSettings(settings) : migrateXmcSettings(settings);
- if (!changed) return mask;
- anyChanged = true;
- return { ...m, settings: migrated };
- });
- if (anyChanged) form.setFieldValue([...base, 'tcp'], next);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
- const isHysteria = protocol === OutboundProtocols.Hysteria || protocol === 'hysteria';
- // Wireguard carries no user-selectable transport (always a UDP listener/
- // dialer), so only the UDP mask section applies — TCP masks would never
- // wrap anything even though the leftover network value may be 'tcp'.
- const isWireguard = protocol === 'wireguard';
- const showTcp = showAll || (!isWireguard && TCP_NETWORKS.includes(network));
- const showUdp = showAll || isHysteria || isWireguard || network === 'kcp';
- const showQuic = showAll || isHysteria || network === 'xhttp';
- const quicParams = Form.useWatch([...base, 'quicParams'], { form, preserve: true });
- const hasQuicParams = quicParams != null;
- if (!showTcp && !showUdp && !showQuic) return null;
- return (
- <>
- {showTcp && <TcpMasksList base={base} form={form} />}
- {showUdp && (
- <UdpMasksList
- base={base}
- form={form}
- isHysteria={isHysteria}
- isWireguard={isWireguard}
- network={network}
- />
- )}
- {showQuic && (
- <>
- <Form.Item label="QUIC Params">
- <Switch
- checked={hasQuicParams}
- onChange={(v) => {
- form.setFieldValue([...base, 'quicParams'], v ? defaultQuicParams() : undefined);
- }}
- />
- </Form.Item>
- {hasQuicParams && <QuicParamsForm base={[...base, 'quicParams']} form={form} />}
- </>
- )}
- </>
- );
- }
- function TcpMasksList({ base, form }: { base: (string | number)[]; form: FormInstance }) {
- const { t } = useTranslation();
- return (
- <Form.List name={[...base, 'tcp']}>
- {(fields, { add, remove }) => (
- <>
- <Form.Item label="TCP Masks">
- <Button
- type="primary"
- size="small"
- icon={<PlusOutlined />}
- aria-label={t('add')}
- 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];
- const { t } = useTranslation();
- return (
- <div>
- <Divider style={{ margin: 0 }}>
- TCP Mask {displayIndex}
- <DeleteOutlined
- className="danger-icon"
- role="button"
- tabIndex={0}
- aria-label={t('remove')}
- onClick={onRemove}
- onKeyDown={activateOnKey(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' },
- { value: 'xmc', label: 'XMC (Minecraft)' },
- ]}
- />
- </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']}
- rules={[{ validator: validateFragmentPackets }]}
- >
- <AutoComplete
- options={[
- { value: 'tlshello', label: 'tlshello' },
- { value: '1-3', label: '1-3' },
- { value: '1-5', label: '1-5' },
- ]}
- placeholder="tlshello or n-m, e.g. 1-3"
- />
- </Form.Item>
- <FragmentRangeList
- listName={[fieldName, 'settings', 'lengths']}
- label="Lengths"
- placeholder="e.g. 100-200"
- minItems={1}
- validator={validateFragmentLength}
- />
- <FragmentRangeList
- listName={[fieldName, 'settings', 'delays']}
- label="Delays"
- placeholder="e.g. 10-20 or 0"
- validator={validateFragmentDelayEntry}
- />
- <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']}>
- <Select mode="tags" style={{ width: '100%' }} tokenSeparators={[',']} />
- </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']}
- />
- );
- }
- if (type === 'xmc') {
- return (
- <>
- <Form.Item label="Hostname" name={[fieldName, 'settings', 'hostname']}>
- <Input placeholder="Server address mimicked in the handshake" />
- </Form.Item>
- <XmcProfilesList tcpFieldName={fieldName} />
- <Form.Item label="Password" required>
- <Space.Compact block>
- <Form.Item
- name={[fieldName, 'settings', 'password']}
- noStyle
- rules={[{ required: true, message: 'Password is required' }]}
- >
- <Input
- placeholder="Obfuscation password"
- style={{ width: 'calc(100% - 32px)' }}
- />
- </Form.Item>
- <Button
- icon={<ReloadOutlined />}
- aria-label={t('regenerate')}
- onClick={() =>
- form.setFieldValue(
- [...absolutePath, 'settings', 'password'],
- RandomUtil.randomLowerAndNum(16),
- )
- }
- />
- </Space.Compact>
- </Form.Item>
- </>
- );
- }
- return null;
- }}
- </Form.Item>
- </div>
- );
- }
- // xray's fragment `packets` accepts "tlshello" or an arbitrary packet-number
- // range like "1-3" (#5075 — presets only covered the common cases).
- function validateFragmentPackets(_rule: unknown, value: unknown): Promise<void> {
- const str = typeof value === 'string' ? value.trim() : String(value ?? '').trim();
- if (str.length === 0 || str === 'tlshello' || /^\d+-\d+$/.test(str)) {
- return Promise.resolve();
- }
- return Promise.reject(new Error('Use "tlshello" or a packet range like 1-3'));
- }
- function validateFragmentLength(_rule: unknown, value: unknown): Promise<void> {
- const str = typeof value === 'string' ? value.trim() : String(value ?? '').trim();
- if (str.length === 0) {
- return Promise.reject(
- new Error('Length is required — xray rejects a fragment mask whose LengthMin is 0'),
- );
- }
- const min = Number(str.split('-')[0]);
- if (!Number.isFinite(min) || min <= 0) {
- return Promise.reject(new Error('Length minimum must be greater than 0 (e.g. 100-200)'));
- }
- 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<void> {
- 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<void>;
- minItems?: number;
- }) {
- const { t } = useTranslation();
- return (
- <Form.List name={listName}>
- {(fields, { add, remove }) => (
- <>
- <Form.Item label={label}>
- <Button
- type="primary"
- size="small"
- icon={<PlusOutlined />}
- aria-label={t('add')}
- onClick={() => add('')}
- />
- </Form.Item>
- {fields.map((field, idx) => (
- <Form.Item
- key={field.key}
- label={`#${idx + 1}`}
- name={field.name}
- rules={validator ? [{ validator }] : undefined}
- >
- <Input
- placeholder={placeholder}
- suffix={
- fields.length > minItems ? (
- <DeleteOutlined
- className="danger-icon"
- role="button"
- tabIndex={0}
- aria-label={t('remove')}
- onClick={() => remove(field.name)}
- onKeyDown={activateOnKey(() => remove(field.name))}
- />
- ) : null
- }
- />
- </Form.Item>
- ))}
- </>
- )}
- </Form.List>
- );
- }
- // randRange bytes must sit in 0-255 — xray rejects the whole config with
- // "invalid randRange" otherwise (reversed ranges like "200-100" are fine,
- // xray reorders them).
- function validateRandRange(_rule: unknown, value: unknown): Promise<void> {
- const str = typeof value === 'string' ? value.trim() : String(value ?? '').trim();
- if (str.length === 0) return Promise.resolve();
- const m = /^(\d{1,3})(?:-(\d{1,3}))?$/.exec(str);
- if (!m) return Promise.reject(new Error('Use a byte value or range like 0-255'));
- const from = Number(m[1]);
- const to = m[2] !== undefined ? Number(m[2]) : from;
- if (from > 255 || to > 255) {
- return Promise.reject(new Error('randRange bytes must be within 0-255'));
- }
- return Promise.resolve();
- }
- 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;
- }
- // Mojang hands the profile UUID back undashed from the session server and
- // dashed from most other endpoints; xray-core parses either, so accept both
- // rather than forcing the operator to reformat what they pasted.
- const XMC_UUID_PATTERN =
- /^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[0-9a-fA-F]{32})$/;
- const XMC_USERNAME_PATTERN = /^[A-Za-z0-9_]{3,16}$/;
- function validateXmcUsername(_rule: unknown, value: unknown): Promise<void> {
- if (typeof value === 'string' && XMC_USERNAME_PATTERN.test(value)) return Promise.resolve();
- return Promise.reject(new Error('3-16 characters, letters/digits/underscore only'));
- }
- function validateXmcUuid(_rule: unknown, value: unknown): Promise<void> {
- if (typeof value === 'string' && XMC_UUID_PATTERN.test(value.trim())) return Promise.resolve();
- return Promise.reject(new Error('Enter the profile UUID (dashed or 32 hex characters)'));
- }
- // Each mask needs at least one fully signed profile since xray-core #6487 —
- // an empty or partial list makes the core reject the whole config, so the
- // panel blocks the save here rather than letting the backend drop the mask.
- function XmcProfilesList({ tcpFieldName }: { tcpFieldName: number }) {
- const { t } = useTranslation();
- return (
- <Form.List name={[tcpFieldName, 'settings', 'profiles']}>
- {(profiles, { add, remove }) => (
- <>
- <Form.Item
- label="Profiles"
- extra="Signed Minecraft session profiles; resolve the UUID by username, then fetch the profile with unsigned=false."
- >
- <Button
- type="primary"
- size="small"
- icon={<PlusOutlined />}
- aria-label={t('add')}
- onClick={() => add(defaultXmcProfile())}
- />
- </Form.Item>
- {profiles.map((profile, idx) => (
- <div key={profile.key}>
- <Divider style={{ margin: 0 }}>
- Profile {idx + 1}
- <DeleteOutlined
- className="danger-icon"
- role="button"
- tabIndex={0}
- aria-label={t('remove')}
- onClick={() => remove(profile.name)}
- onKeyDown={activateOnKey(() => remove(profile.name))}
- />
- </Divider>
- <Form.Item
- label="Username"
- name={[profile.name, 'username']}
- rules={[{ validator: validateXmcUsername }]}
- >
- <Input placeholder="Notch" />
- </Form.Item>
- <Form.Item
- label="UUID"
- name={[profile.name, 'uuid']}
- rules={[{ validator: validateXmcUuid }]}
- >
- <Input placeholder="069a79f4-44e9-4726-a5be-fca90e38aaf5" />
- </Form.Item>
- <Form.Item
- label="Textures Value"
- name={[profile.name, 'texturesValue']}
- rules={[{ required: true, message: 'Textures value is required' }]}
- >
- <Input.TextArea
- autoSize={{ minRows: 2, maxRows: 4 }}
- placeholder="Base64 value from the session profile"
- />
- </Form.Item>
- <Form.Item
- label="Textures Signature"
- name={[profile.name, 'texturesSignature']}
- rules={[{ required: true, message: 'Textures signature is required' }]}
- >
- <Input.TextArea
- autoSize={{ minRows: 2, maxRows: 4 }}
- placeholder="Base64 signature from the session profile"
- />
- </Form.Item>
- </div>
- ))}
- </>
- )}
- </Form.List>
- );
- }
- function HeaderCustomGroups({
- tcpFieldName,
- form,
- absoluteSettingsPath,
- }: {
- tcpFieldName: number;
- form: FormInstance;
- absoluteSettingsPath: (string | number)[];
- }) {
- const { t } = useTranslation();
- 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 />}
- aria-label={t('add')}
- 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"
- role="button"
- tabIndex={0}
- aria-label={t('remove')}
- onClick={() => removeGroup(group.name)}
- onKeyDown={activateOnKey(() => removeGroup(group.name))}
- />
- </Divider>
- <Form.List name={[group.name]}>
- {(items, { add: addItem, remove: removeItem }) => (
- <>
- <Form.Item label="Items">
- <Button
- size="small"
- icon={<PlusOutlined />}
- aria-label={t('add')}
- 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,
- isWireguard,
- network,
- }: {
- base: (string | number)[];
- form: FormInstance;
- isHysteria: boolean;
- isWireguard: boolean;
- network: string;
- }) {
- const { t } = useTranslation();
- return (
- <Form.List name={[...base, 'udp']}>
- {(fields, { add, remove }) => (
- <>
- <Form.Item label="UDP Masks">
- <Button
- type="primary"
- size="small"
- icon={<PlusOutlined />}
- aria-label={t('add')}
- onClick={() => {
- const def = isHysteria || isWireguard ? 'salamander' : 'mkcp-legacy';
- 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}
- isWireguard={isWireguard}
- network={network}
- onRemove={() => remove(field.name)}
- />
- ))}
- </>
- )}
- </Form.List>
- );
- }
- function UdpMaskItem({
- fieldName,
- displayIndex,
- form,
- listPath,
- isHysteria,
- isWireguard,
- network,
- onRemove,
- }: {
- fieldName: number;
- displayIndex: number;
- form: FormInstance;
- listPath: (string | number)[];
- isHysteria: boolean;
- isWireguard: boolean;
- network: string;
- onRemove: () => void;
- }) {
- const absolutePath = [...listPath, fieldName];
- const { t } = useTranslation();
- 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)' }]
- : [
- // Salamander is the mask xray-core's own wireguard finalmask example
- // uses; it stays hysteria-only elsewhere to keep legacy parity.
- ...(isWireguard ? [{ value: 'salamander', label: 'Salamander' }] : []),
- { value: 'mkcp-legacy', label: 'mKCP Legacy' },
- { value: 'xdns', label: 'xDNS' },
- { value: 'xicmp', label: 'xICMP' },
- { value: 'realm', label: 'Realm' },
- { value: 'header-custom', label: 'Header Custom' },
- { value: 'noise', label: 'Noise' },
- ];
- return (
- <div>
- <Divider style={{ margin: 0 }}>
- UDP Mask {displayIndex}
- <DeleteOutlined
- className="danger-icon"
- role="button"
- tabIndex={0}
- aria-label={t('remove')}
- onClick={onRemove}
- onKeyDown={activateOnKey(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 === 'salamander') {
- return (
- <SalamanderUdpMaskSettings
- fieldName={fieldName}
- form={form}
- absolutePath={absolutePath}
- />
- );
- }
- if (type === 'mkcp-legacy') {
- return (
- <>
- <Form.Item label="Header" name={[fieldName, 'settings', 'header']}>
- <Select
- options={[
- { value: '', label: 'Original / AES-128-GCM' },
- { value: 'dns', label: 'DNS' },
- { value: 'dtls', label: 'DTLS 1.2' },
- { value: 'srtp', label: 'SRTP' },
- { value: 'utp', label: 'uTP' },
- { value: 'wechat', label: 'WeChat Video' },
- { value: 'wireguard', label: 'WireGuard' },
- ]}
- />
- </Form.Item>
- <Form.Item label="Value" name={[fieldName, 'settings', 'value']}>
- <Input placeholder="password (AES-128-GCM) or domain (DNS header)" />
- </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="Dgram"
- name={[fieldName, 'settings', 'dgram']}
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item label="IPs" name={[fieldName, 'settings', 'ips']}>
- <Select mode="tags" style={{ width: '100%' }} tokenSeparators={[',']} />
- </Form.Item>
- </>
- );
- }
- if (type === 'realm') {
- return (
- <>
- <Form.Item label="URL" name={[fieldName, 'settings', 'url']}>
- <Input placeholder="realm://token@host:port/id" />
- </Form.Item>
- <Form.Item label="STUN Servers" name={[fieldName, 'settings', 'stunServers']}>
- <Select
- mode="tags"
- style={{ width: '100%' }}
- tokenSeparators={[',']}
- placeholder="host:port"
- />
- </Form.Item>
- <Form.Item label="IP Mode" name={[fieldName, 'settings', 'ipMode']}>
- <Select
- allowClear
- placeholder="dual"
- options={[
- { value: 'dual', label: 'Dual' },
- { value: 'v4', label: 'IPv4' },
- { value: 'v6', label: 'IPv6' },
- ]}
- />
- </Form.Item>
- <Form.Item
- label="Port Mapping (UPnP / NAT-PMP)"
- name={[fieldName, 'settings', 'portMapping', 'enabled']}
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item
- label="Mapping Timeout (s)"
- name={[fieldName, 'settings', 'portMapping', 'timeout']}
- >
- <InputNumber min={0} placeholder="10 = default" />
- </Form.Item>
- <Form.Item
- label="Mapping Lifetime (s)"
- name={[fieldName, 'settings', 'portMapping', 'lifetime']}
- >
- <InputNumber min={0} placeholder="600 = default" />
- </Form.Item>
- <Divider plain style={{ margin: '8px 0' }}>
- TLS (optional)
- </Divider>
- <Form.Item
- label="Server Name"
- name={[fieldName, 'settings', 'tlsConfig', 'serverName']}
- >
- <Input placeholder="SNI for the realm server (leave empty to skip TLS)" />
- </Form.Item>
- <Form.Item label="ALPN" name={[fieldName, 'settings', 'tlsConfig', 'alpn']}>
- <Select
- mode="multiple"
- style={{ width: '100%' }}
- options={[
- { value: 'h3', label: 'h3' },
- { value: 'h2', label: 'h2' },
- { value: 'http/1.1', label: 'http/1.1' },
- ]}
- />
- </Form.Item>
- <Form.Item
- label="Fingerprint"
- name={[fieldName, 'settings', 'tlsConfig', 'fingerprint']}
- >
- <Select allowClear style={{ width: '100%' }} options={UTLS_FINGERPRINT_OPTIONS} />
- </Form.Item>
- <Form.Item
- label="Allow Insecure"
- name={[fieldName, 'settings', 'tlsConfig', 'allowInsecure']}
- valuePropName="checked"
- >
- <Switch />
- </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 SalamanderUdpMaskSettings({
- fieldName,
- form,
- absolutePath,
- }: {
- fieldName: number;
- form: FormInstance;
- absolutePath: (string | number)[];
- }) {
- const { t } = useTranslation();
- const packetSizePath = [...absolutePath, 'settings', 'packetSize'];
- const packetSize = Form.useWatch(packetSizePath, { form, preserve: true });
- const mode = typeof packetSize === 'string' && packetSize.trim() !== '' ? 'gecko' : 'salamander';
- return (
- <>
- <Form.Item
- label="Mode"
- extra={
- mode === 'gecko'
- ? 'Salamander plus Gecko: splits each packet into random-padded fragments sized within the range below, defeating packet-length fingerprinting. Stored as Salamander with packetSize.'
- : 'Scrambles each packet into random-looking bytes.'
- }
- >
- <Select
- value={mode}
- onChange={(next) => {
- if (next === 'gecko') {
- const current = form.getFieldValue(packetSizePath);
- form.setFieldValue(
- packetSizePath,
- parseGeckoPacketSize(current)
- ? current
- : formatGeckoPacketSize(
- DEFAULT_GECKO_PACKET_SIZE.min,
- DEFAULT_GECKO_PACKET_SIZE.max,
- ),
- );
- } else {
- form.setFieldValue(packetSizePath, undefined);
- }
- }}
- options={[
- { value: 'salamander', label: 'Salamander' },
- { value: 'gecko', label: 'Gecko experimental' },
- ]}
- />
- </Form.Item>
- <Form.Item label="Password">
- <Space.Compact block>
- <Form.Item name={[fieldName, 'settings', 'password']} noStyle>
- <Input placeholder="Obfuscation password" style={{ width: 'calc(100% - 32px)' }} />
- </Form.Item>
- <Button
- icon={<ReloadOutlined />}
- aria-label={t('regenerate')}
- onClick={() =>
- form.setFieldValue(
- [...absolutePath, 'settings', 'password'],
- RandomUtil.randomLowerAndNum(16),
- )
- }
- />
- </Space.Compact>
- </Form.Item>
- {mode === 'gecko' && (
- <Form.Item
- label="Packet size"
- name={[fieldName, 'settings', 'packetSize']}
- rules={[{ validator: validateGeckoPacketSize }]}
- extra="Serialized as a string range, for example 512-1200."
- >
- <GeckoPacketSizeInput />
- </Form.Item>
- )}
- </>
- );
- }
- function GeckoPacketSizeInput({
- value,
- onChange,
- }: {
- value?: string;
- onChange?: (value: string) => void;
- }) {
- const { min, max } = splitGeckoPacketSize(value);
- return (
- <Space.Compact block>
- <InputNumber
- prefix="Min"
- min={GECKO_MIN_PACKET_SIZE}
- max={GECKO_MAX_PACKET_SIZE}
- precision={0}
- value={min}
- placeholder={String(DEFAULT_GECKO_PACKET_SIZE.min)}
- onChange={(next) => onChange?.(`${next ?? ''}-${max ?? ''}`)}
- style={{ width: '50%' }}
- />
- <InputNumber
- prefix="Max"
- min={GECKO_MIN_PACKET_SIZE}
- max={GECKO_MAX_PACKET_SIZE}
- precision={0}
- value={max}
- placeholder={String(DEFAULT_GECKO_PACKET_SIZE.max)}
- onChange={(next) => onChange?.(`${min ?? ''}-${next ?? ''}`)}
- style={{ width: '50%' }}
- />
- </Space.Compact>
- );
- }
- function UdpHeaderCustom({
- udpFieldName,
- form,
- absoluteSettingsPath,
- }: {
- udpFieldName: number;
- form: FormInstance;
- absoluteSettingsPath: (string | number)[];
- }) {
- const { t } = useTranslation();
- 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 />}
- aria-label={t('add')}
- 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"
- role="button"
- tabIndex={0}
- aria-label={t('remove')}
- onClick={() => remove(item.name)}
- onKeyDown={activateOnKey(() => 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)[];
- }) {
- const { t } = useTranslation();
- 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 />}
- aria-label={t('add')}
- onClick={() => add(defaultNoiseItem())}
- />
- </Form.Item>
- {items.map((item, ni) => (
- <div key={item.key}>
- <Divider style={{ margin: 0 }}>
- Noise {ni + 1}
- <DeleteOutlined
- className="danger-icon"
- role="button"
- tabIndex={0}
- aria-label={t('remove')}
- onClick={() => remove(item.name)}
- onKeyDown={activateOnKey(() => 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 { t } = useTranslation();
- /**
- * Switching to `array` clears the packet instead of emptying it to `[]`:
- * that branch is rand-driven, and xray-core counts even an empty array as a
- * packet, rejecting an item that carries both a packet and a rand. That
- * error fails the whole config, so one such item keeps every inbound offline.
- */
- 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'], undefined);
- } 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>
- {/* Cleared must become undefined, not '': xray parses an
- explicit "" as the range 0-0 (all-zero fill bytes), while
- an omitted randRange falls back to the 0-255 default. */}
- <Form.Item
- label="Rand Range"
- name={[fieldName, 'randRange']}
- normalize={(v) => (v === '' ? undefined : v)}
- rules={[{ validator: validateRandRange }]}
- >
- <Input placeholder="0-255" />
- </Form.Item>
- </>
- );
- }
- if (type === 'base64') {
- return (
- <Form.Item label="Packet">
- <Space.Compact block>
- <Form.Item name={[fieldName, 'packet']} noStyle>
- <Input placeholder="binary data" style={{ width: 'calc(100% - 32px)' }} />
- </Form.Item>
- <Button
- icon={<ReloadOutlined />}
- aria-label={t('regenerate')}
- onClick={() =>
- form.setFieldValue([...absoluteItemPath, 'packet'], RandomUtil.randomBase64())
- }
- />
- </Space.Compact>
- </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 udpHop = Form.useWatch([...base, 'udpHop'], { form, preserve: true }) as
- | Record<string, unknown>
- | undefined;
- const hasUdpHop = udpHop != null;
- 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>
- {congestion === 'bbr' && (
- <Form.Item label="BBR Profile" name={[...base, 'bbrProfile']}>
- <Select
- allowClear
- placeholder="standard"
- options={[
- { value: 'conservative', label: 'Conservative' },
- { value: 'standard', label: 'Standard' },
- { value: 'aggressive', label: 'Aggressive' },
- ]}
- />
- </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="e.g. 60 mbps" />
- </Form.Item>
- <Form.Item label="Brutal Down" name={[...base, 'brutalDown']}>
- <Input placeholder="e.g. 100 mbps" />
- </Form.Item>
- <Form.Item
- label="Brutal Disable Loss Comp"
- name={[...base, 'brutalDisableLossCompensation']}
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- </>
- )}
- <Form.Item label="UDP Hop">
- <Switch
- checked={hasUdpHop}
- onChange={(v) => {
- form.setFieldValue([...base, 'udpHop'], v ? defaultUdpHop() : undefined);
- }}
- />
- </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="Disable Chrome Parrot"
- name={[...base, 'disableChromeParrot']}
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item label="Disable GSO" name={[...base, 'disableGSO']} valuePropName="checked">
- <Switch />
- </Form.Item>
- <Form.Item
- label="Disable Stateless Reset"
- name={[...base, 'disableStatelessReset']}
- 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>
- </>
- );
- }
|