mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-29 08:29:34 +00:00
Replace the 'edit via JSON' deferred-features hint with the full XHTTP sub-form matching the legacy modal's XhttpFields helper. schemas/protocols/stream/xhttp.ts: - New XHttpXmuxSchema: 6 connection-multiplexing knobs (maxConcurrency, maxConnections, cMaxReuseTimes, hMaxRequestTimes, hMaxReusableSecs, hKeepAlivePeriod). - XHttpStreamSettingsSchema gains 5 outbound-only fields and one UI-only toggle: scMinPostsIntervalMs, uplinkChunkSize, noGRPCHeader, xmux, enableXmux. outbound-form-adapter.ts: - New stripUiOnlyStreamFields() drops xhttpSettings.enableXmux on the way to wire so the panel never embeds the UI toggle into the saved config. xray-core ignores unknown fields anyway, but the panel reads back its own emitted JSON, so a clean wire shape matters. OutboundFormModal.tsx: - Headers editor (HeaderMapEditor v1) for xhttpSettings.headers. - Padding obfs Switch + 4 conditional fields (key/header/placement/ method) when on. - Uplink HTTP method Select with GET disabled outside packet-up. - Session placement + session key (key shown when placement != path). - Sequence placement + sequence key (same pattern). - packet-up mode: scMinPostsIntervalMs, scMaxEachPostBytes, uplink data placement + key + chunk size (key/chunk-size shown when placement != body). - stream-up / stream-one mode: noGRPCHeader Switch. - XMUX Switch + 6 nested fields when on.
2103 lines
99 KiB
TypeScript
2103 lines
99 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
Button,
|
|
Form,
|
|
Input,
|
|
InputNumber,
|
|
Modal,
|
|
Radio,
|
|
Select,
|
|
Space,
|
|
Switch,
|
|
Tabs,
|
|
message,
|
|
} from 'antd';
|
|
import { DeleteOutlined, MinusOutlined, PlusOutlined, SyncOutlined } from '@ant-design/icons';
|
|
|
|
import HeaderMapEditor from '@/components/HeaderMapEditor';
|
|
import InputAddon from '@/components/InputAddon';
|
|
import JsonEditor from '@/components/JsonEditor';
|
|
import { Wireguard } from '@/utils';
|
|
import {
|
|
formValuesToWirePayload,
|
|
rawOutboundToFormValues,
|
|
} from '@/lib/xray/outbound-form-adapter';
|
|
import {
|
|
OutboundFormBaseSchema,
|
|
ShadowsocksOutboundFormSettingsSchema,
|
|
TrojanOutboundFormSettingsSchema,
|
|
VlessOutboundFormSettingsSchema,
|
|
VmessOutboundFormSettingsSchema,
|
|
type OutboundFormValues,
|
|
} from '@/schemas/forms/outbound-form';
|
|
import {
|
|
ALPN_OPTION,
|
|
Address_Port_Strategy,
|
|
DNSRuleActions,
|
|
MODE_OPTION,
|
|
OutboundDomainStrategies,
|
|
OutboundProtocols as Protocols,
|
|
SNIFFING_OPTION,
|
|
TCP_CONGESTION_OPTION,
|
|
TLS_FLOW_CONTROL,
|
|
USERS_SECURITY,
|
|
UTLS_FINGERPRINT,
|
|
WireguardDomainStrategy,
|
|
} from '@/schemas/primitives';
|
|
import {
|
|
canEnableReality,
|
|
canEnableStream,
|
|
canEnableTls,
|
|
canEnableTlsFlow,
|
|
} from '@/lib/xray/protocol-capabilities';
|
|
import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
|
|
import { antdRule } from '@/utils/zodForm';
|
|
import './OutboundFormModal.css';
|
|
|
|
// Pattern A rewrite of OutboundFormModal. Built as a sibling `.new.tsx`
|
|
// file so the build stays green section-by-section. The atomic swap at
|
|
// the end of the rewrite replaces the legacy file in one commit
|
|
// (per Core Decision 7 in the migration spec).
|
|
|
|
interface OutboundFormModalProps {
|
|
open: boolean;
|
|
outbound: Record<string, unknown> | null;
|
|
existingTags: string[];
|
|
onClose: () => void;
|
|
onConfirm: (outbound: Record<string, unknown>) => void;
|
|
}
|
|
|
|
const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
|
|
const SECURITY_OPTIONS = Object.values(USERS_SECURITY).map((v) => ({ value: v, label: v }));
|
|
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL).map((v) => ({ value: v, label: v }));
|
|
const SS_METHOD_OPTIONS = SSMethodSchema.options.map((v) => ({ value: v, label: v }));
|
|
const MODE_OPTIONS = Object.values(MODE_OPTION).map((v) => ({ value: v, label: v }));
|
|
const UTLS_OPTIONS = Object.values(UTLS_FINGERPRINT).map((v) => ({ value: v, label: v }));
|
|
const ALPN_OPTIONS = Object.values(ALPN_OPTION).map((v) => ({ value: v, label: v }));
|
|
const ADDRESS_PORT_STRATEGY_OPTIONS = Object.values(Address_Port_Strategy).map((v) => ({
|
|
value: v,
|
|
label: v,
|
|
}));
|
|
|
|
// canEnableMux mirrors the adapter's helper but lives here so the modal
|
|
// can show/hide the Mux section without going through the adapter.
|
|
const MUX_PROTOCOLS = new Set<string>(['vmess', 'vless', 'trojan', 'shadowsocks', 'http', 'socks']);
|
|
function isMuxAllowed(protocol: string, flow: string, network: string): boolean {
|
|
if (!MUX_PROTOCOLS.has(protocol)) return false;
|
|
if (protocol === 'vless' && flow) return false;
|
|
if (network === 'xhttp') return false;
|
|
return true;
|
|
}
|
|
|
|
const NETWORK_OPTIONS: { value: string; label: string }[] = [
|
|
{ value: 'tcp', label: 'TCP (RAW)' },
|
|
{ value: 'kcp', label: 'mKCP' },
|
|
{ value: 'ws', label: 'WebSocket' },
|
|
{ value: 'grpc', label: 'gRPC' },
|
|
{ value: 'httpupgrade', label: 'HTTPUpgrade' },
|
|
{ value: 'xhttp', label: 'XHTTP' },
|
|
];
|
|
|
|
// Hysteria appends an extra `hysteria` network branch to the selector
|
|
// — only when the parent protocol is hysteria. Wire-side this matches
|
|
// the legacy modal's `isHysteria ? [...NETWORKS, 'hysteria'] : NETWORKS`.
|
|
const HYSTERIA_NETWORK_OPTION = { value: 'hysteria', label: 'Hysteria' };
|
|
|
|
// Per-network bootstrap. Mirrors the legacy class constructors so the
|
|
// initial state for each transport matches what xray-core expects.
|
|
function newStreamSlice(network: string): Record<string, unknown> {
|
|
switch (network) {
|
|
case 'tcp':
|
|
return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
|
|
case 'kcp':
|
|
return {
|
|
network: 'kcp',
|
|
kcpSettings: {
|
|
mtu: 1350, tti: 20, uplinkCapacity: 5, downlinkCapacity: 20,
|
|
cwndMultiplier: 1, maxSendingWindow: 2097152,
|
|
},
|
|
};
|
|
case 'ws':
|
|
return {
|
|
network: 'ws',
|
|
wsSettings: { path: '/', host: '', headers: {}, heartbeatPeriod: 0 },
|
|
};
|
|
case 'grpc':
|
|
return {
|
|
network: 'grpc',
|
|
grpcSettings: { serviceName: '', authority: '', multiMode: false },
|
|
};
|
|
case 'httpupgrade':
|
|
return {
|
|
network: 'httpupgrade',
|
|
httpupgradeSettings: { path: '/', host: '', headers: {} },
|
|
};
|
|
case 'xhttp':
|
|
return {
|
|
network: 'xhttp',
|
|
xhttpSettings: {
|
|
path: '/', host: '', mode: '', headers: [],
|
|
xPaddingBytes: '100-1000', scMaxEachPostBytes: '1000000',
|
|
},
|
|
};
|
|
case 'hysteria':
|
|
return {
|
|
network: 'hysteria',
|
|
hysteriaSettings: {
|
|
version: 2,
|
|
auth: '',
|
|
congestion: '',
|
|
up: '0',
|
|
down: '0',
|
|
initStreamReceiveWindow: 8388608,
|
|
maxStreamReceiveWindow: 8388608,
|
|
initConnectionReceiveWindow: 20971520,
|
|
maxConnectionReceiveWindow: 20971520,
|
|
maxIdleTimeout: 30,
|
|
keepAlivePeriod: 2,
|
|
disablePathMTUDiscovery: false,
|
|
},
|
|
};
|
|
default:
|
|
return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
|
|
}
|
|
}
|
|
|
|
// Protocols whose form schema carries a flat connect target — these all
|
|
// get the shared "server" sub-block (address + port) at the top of the
|
|
// protocol section. Wireguard has an address but no port. DNS/freedom/
|
|
// blackhole/loopback have no connect target.
|
|
const SERVER_PROTOCOLS = new Set<string>([
|
|
'vmess', 'vless', 'trojan', 'shadowsocks', 'socks', 'http', 'hysteria',
|
|
]);
|
|
|
|
function buildAddModeValues(): OutboundFormValues {
|
|
return rawOutboundToFormValues({});
|
|
}
|
|
|
|
export default function OutboundFormModal({
|
|
open,
|
|
outbound: outboundProp,
|
|
existingTags,
|
|
onClose,
|
|
onConfirm,
|
|
}: OutboundFormModalProps) {
|
|
const { t } = useTranslation();
|
|
const [messageApi, messageContextHolder] = message.useMessage();
|
|
const [form] = Form.useForm<OutboundFormValues>();
|
|
const [activeKey, setActiveKey] = useState('1');
|
|
const [jsonText, setJsonText] = useState('');
|
|
const [jsonDirty, setJsonDirty] = useState(false);
|
|
|
|
const isEdit = outboundProp != null;
|
|
const title = isEdit
|
|
? `${t('edit')} ${t('pages.xray.Outbounds')}`
|
|
: `+ ${t('pages.xray.Outbounds')}`;
|
|
const okText = isEdit ? t('pages.clients.submitEdit') : t('create');
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const initial = outboundProp
|
|
? rawOutboundToFormValues(outboundProp)
|
|
: buildAddModeValues();
|
|
form.resetFields();
|
|
form.setFieldsValue(initial);
|
|
setActiveKey('1');
|
|
setJsonText(JSON.stringify(formValuesToWirePayload(initial), null, 2));
|
|
setJsonDirty(false);
|
|
}, [open, outboundProp, form]);
|
|
|
|
const tag = Form.useWatch('tag', form) ?? '';
|
|
const protocol = (Form.useWatch('protocol', form) ?? 'vless') as string;
|
|
const network = (Form.useWatch(['streamSettings', 'network'], form) ?? '') as string;
|
|
const security = (Form.useWatch(['streamSettings', 'security'], form) ?? 'none') as string;
|
|
|
|
const streamAllowed = canEnableStream({ protocol });
|
|
const tlsAllowed = canEnableTls({ protocol, streamSettings: { network, security } });
|
|
const realityAllowed = canEnableReality({ protocol, streamSettings: { network, security } });
|
|
const tlsFlowAllowed = canEnableTlsFlow({ protocol, streamSettings: { network, security } });
|
|
|
|
// Seed streamSettings when the user picks a protocol that supports
|
|
// streams but the form does not yet have a stream slice (new outbound,
|
|
// or wire payload arrived without streamSettings).
|
|
useEffect(() => {
|
|
if (!streamAllowed) return;
|
|
if (network) return;
|
|
form.setFieldValue('streamSettings', { ...newStreamSlice('tcp'), security: 'none' });
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [streamAllowed, network]);
|
|
|
|
// Wireguard pubKey is a UI-only field derived from secretKey on every
|
|
// edit. The legacy modal did the same on every keystroke. We re-derive
|
|
// here so paste-in secret keys immediately surface the matching pub.
|
|
const wgSecretKey = Form.useWatch(['settings', 'secretKey'], form) as string | undefined;
|
|
useEffect(() => {
|
|
if (protocol !== 'wireguard') return;
|
|
const sk = (wgSecretKey ?? '').trim();
|
|
if (!sk) {
|
|
form.setFieldValue(['settings', 'pubKey'], '');
|
|
return;
|
|
}
|
|
try {
|
|
const { publicKey } = Wireguard.generateKeypair(sk);
|
|
form.setFieldValue(['settings', 'pubKey'], publicKey);
|
|
} catch {
|
|
form.setFieldValue(['settings', 'pubKey'], '');
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [protocol, wgSecretKey]);
|
|
|
|
// Switching protocol resets the settings sub-object to fresh defaults
|
|
// so leftover fields from the previous protocol do not bleed through.
|
|
// The adapter's rawOutboundToFormValues seeds whatever the new protocol
|
|
// expects (vless flat shape, vmess flat shape, wireguard with secretKey
|
|
// placeholder, etc.).
|
|
function onValuesChange(changed: Partial<OutboundFormValues>) {
|
|
if ('protocol' in changed && changed.protocol) {
|
|
const next = rawOutboundToFormValues({ protocol: changed.protocol });
|
|
form.setFieldValue('settings', next.settings);
|
|
}
|
|
}
|
|
|
|
// Security change cascade: swap the security sub-key so the DU branch
|
|
// matches. Seed default field values when entering tls/reality so the
|
|
// sub-forms render without `undefined` field references.
|
|
function onSecurityChange(next: string) {
|
|
const stream = form.getFieldValue('streamSettings') ?? {};
|
|
const cleaned = { ...stream } as Record<string, unknown>;
|
|
delete cleaned.tlsSettings;
|
|
delete cleaned.realitySettings;
|
|
if (next === 'tls') {
|
|
cleaned.tlsSettings = {
|
|
serverName: '',
|
|
alpn: [],
|
|
fingerprint: '',
|
|
echConfigList: '',
|
|
verifyPeerCertByName: '',
|
|
pinnedPeerCertSha256: '',
|
|
};
|
|
} else if (next === 'reality') {
|
|
cleaned.realitySettings = {
|
|
publicKey: '',
|
|
fingerprint: 'chrome',
|
|
serverName: '',
|
|
shortId: '',
|
|
spiderX: '',
|
|
mldsa65Verify: '',
|
|
};
|
|
}
|
|
cleaned.security = next;
|
|
form.setFieldValue('streamSettings', cleaned);
|
|
}
|
|
|
|
// Network change cascade: swap the per-network sub-key (tcpSettings,
|
|
// wsSettings, etc.) so the DU branch matches. Preserve security if
|
|
// the new network supports it, otherwise force back to 'none'.
|
|
function onNetworkChange(next: string) {
|
|
const currentSecurity = form.getFieldValue(['streamSettings', 'security']) ?? 'none';
|
|
const stillAllowed = canEnableTls({ protocol, streamSettings: { network: next, security: currentSecurity } });
|
|
const stillReality = canEnableReality({ protocol, streamSettings: { network: next, security: currentSecurity } });
|
|
const newSecurity =
|
|
currentSecurity === 'tls' && !stillAllowed
|
|
? 'none'
|
|
: currentSecurity === 'reality' && !stillReality
|
|
? 'none'
|
|
: currentSecurity;
|
|
form.setFieldValue('streamSettings', { ...newStreamSlice(next), security: newSecurity });
|
|
}
|
|
|
|
const duplicateTag = useMemo(() => {
|
|
const myTag = tag.trim();
|
|
if (!myTag) return false;
|
|
if (isEdit && (outboundProp?.tag as string | undefined) === myTag) return false;
|
|
return (existingTags || []).includes(myTag);
|
|
}, [tag, existingTags, isEdit, outboundProp]);
|
|
|
|
// Bridge form ↔ JSON tab: when leaving the JSON tab back to Basic, push
|
|
// any edits into form state. When entering JSON tab, snapshot current
|
|
// form values so the user sees the live shape.
|
|
function applyJsonToForm(): boolean {
|
|
if (!jsonDirty) return true;
|
|
const raw = jsonText.trim();
|
|
if (!raw) return true;
|
|
let parsed: Record<string, unknown>;
|
|
try {
|
|
parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
} catch (e) {
|
|
messageApi.error(`JSON: ${(e as Error).message}`);
|
|
return false;
|
|
}
|
|
const next = rawOutboundToFormValues(parsed);
|
|
form.resetFields();
|
|
form.setFieldsValue(next);
|
|
setJsonDirty(false);
|
|
return true;
|
|
}
|
|
|
|
function onTabChange(key: string) {
|
|
if (document.activeElement instanceof HTMLElement) {
|
|
document.activeElement.blur();
|
|
}
|
|
if (key === '2') {
|
|
const values = form.getFieldsValue(true) as OutboundFormValues;
|
|
setJsonText(JSON.stringify(formValuesToWirePayload(values), null, 2));
|
|
setJsonDirty(false);
|
|
setActiveKey(key);
|
|
return;
|
|
}
|
|
if (key === '1' && activeKey === '2') {
|
|
if (!applyJsonToForm()) return;
|
|
}
|
|
setActiveKey(key);
|
|
}
|
|
|
|
async function onOk() {
|
|
if (activeKey === '2' && !applyJsonToForm()) return;
|
|
let values: OutboundFormValues;
|
|
try {
|
|
values = await form.validateFields();
|
|
} catch {
|
|
return;
|
|
}
|
|
if (duplicateTag) {
|
|
messageApi.error('Tag already used by another outbound');
|
|
return;
|
|
}
|
|
onConfirm(formValuesToWirePayload(values));
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{messageContextHolder}
|
|
<Modal
|
|
open={open}
|
|
title={title}
|
|
okText={okText}
|
|
cancelText={t('close')}
|
|
mask={{ closable: false }}
|
|
width={780}
|
|
onOk={onOk}
|
|
onCancel={onClose}
|
|
destroyOnHidden
|
|
>
|
|
<Form
|
|
form={form}
|
|
colon={false}
|
|
labelCol={{ md: { span: 8 } }}
|
|
wrapperCol={{ md: { span: 14 } }}
|
|
onValuesChange={onValuesChange}
|
|
>
|
|
<Tabs
|
|
activeKey={activeKey}
|
|
onChange={onTabChange}
|
|
items={[
|
|
{
|
|
key: '1',
|
|
label: t('pages.xray.basicTemplate'),
|
|
children: (
|
|
<>
|
|
<Form.Item
|
|
label={t('protocol')}
|
|
name="protocol"
|
|
rules={[antdRule(OutboundFormBaseSchema.shape.tag, t)]}
|
|
>
|
|
<Select options={PROTOCOL_OPTIONS} />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="Tag"
|
|
name="tag"
|
|
validateStatus={duplicateTag ? 'warning' : undefined}
|
|
help={duplicateTag ? 'Tag already used by another outbound' : undefined}
|
|
rules={[
|
|
{ required: true, message: 'Tag is required' },
|
|
]}
|
|
>
|
|
<Input placeholder="unique-tag" />
|
|
</Form.Item>
|
|
|
|
<Form.Item label="Send through" name="sendThrough">
|
|
<Input placeholder="local IP" />
|
|
</Form.Item>
|
|
|
|
{/* Shared connect target (address + port) for protocols
|
|
whose form schema carries them flat at settings root.
|
|
Hidden for freedom/blackhole/dns/loopback/wireguard. */}
|
|
{SERVER_PROTOCOLS.has(protocol) && (
|
|
<>
|
|
<Form.Item
|
|
label={t('pages.inbounds.address')}
|
|
name={['settings', 'address']}
|
|
rules={[{ required: true, message: 'Address is required' }]}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('pages.inbounds.port')}
|
|
name={['settings', 'port']}
|
|
rules={[{ required: true, message: 'Port is required' }]}
|
|
>
|
|
<InputNumber min={1} max={65535} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
|
|
{(protocol === 'vmess' || protocol === 'vless') && (
|
|
<Form.Item
|
|
label="ID"
|
|
name={['settings', 'id']}
|
|
rules={[antdRule(VmessOutboundFormSettingsSchema.shape.id, t)]}
|
|
>
|
|
<Input placeholder="UUID" />
|
|
</Form.Item>
|
|
)}
|
|
{protocol === 'vmess' && (
|
|
<Form.Item
|
|
label={t('security')}
|
|
name={['settings', 'security']}
|
|
rules={[antdRule(VmessOutboundFormSettingsSchema.shape.security, t)]}
|
|
>
|
|
<Select options={SECURITY_OPTIONS} />
|
|
</Form.Item>
|
|
)}
|
|
{protocol === 'vless' && (
|
|
<>
|
|
<Form.Item
|
|
label={t('encryption')}
|
|
name={['settings', 'encryption']}
|
|
rules={[antdRule(VlessOutboundFormSettingsSchema.shape.encryption, t)]}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item label="Reverse tag" name={['settings', 'reverseTag']}>
|
|
<Input placeholder="optional" />
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
|
|
{(protocol === 'trojan' || protocol === 'shadowsocks') && (
|
|
<Form.Item
|
|
label={t('password')}
|
|
name={['settings', 'password']}
|
|
rules={[
|
|
antdRule(
|
|
protocol === 'trojan'
|
|
? TrojanOutboundFormSettingsSchema.shape.password
|
|
: ShadowsocksOutboundFormSettingsSchema.shape.password,
|
|
t,
|
|
),
|
|
]}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
)}
|
|
|
|
{protocol === 'shadowsocks' && (
|
|
<>
|
|
<Form.Item
|
|
label={t('encryption')}
|
|
name={['settings', 'method']}
|
|
rules={[antdRule(SSMethodSchema, t)]}
|
|
>
|
|
<Select options={SS_METHOD_OPTIONS} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="UDP over TCP"
|
|
name={['settings', 'uot']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item label="UoT version" name={['settings', 'UoTVersion']}>
|
|
<InputNumber min={1} max={2} />
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
|
|
{(protocol === 'socks' || protocol === 'http') && (
|
|
<>
|
|
<Form.Item label={t('username')} name={['settings', 'user']}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item label={t('password')} name={['settings', 'pass']}>
|
|
<Input />
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
|
|
{protocol === 'hysteria' && (
|
|
<Form.Item label="Version" name={['settings', 'version']}>
|
|
<InputNumber min={2} max={2} disabled />
|
|
</Form.Item>
|
|
)}
|
|
|
|
{protocol === 'loopback' && (
|
|
<Form.Item label="Inbound tag" name={['settings', 'inboundTag']}>
|
|
<Input placeholder="inbound tag used in routing rules" />
|
|
</Form.Item>
|
|
)}
|
|
|
|
{protocol === 'blackhole' && (
|
|
<Form.Item label="Response type" name={['settings', 'type']}>
|
|
<Select
|
|
options={[
|
|
{ value: '', label: '(empty)' },
|
|
{ value: 'none', label: 'none' },
|
|
{ value: 'http', label: 'http' },
|
|
]}
|
|
/>
|
|
</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'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']}>
|
|
<Input placeholder="comma-separated, e.g. 10.0.0.1,fd00::1" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={
|
|
<>
|
|
{t('pages.inbounds.privatekey')}
|
|
<SyncOutlined
|
|
className="random-icon"
|
|
onClick={() => {
|
|
const pair = Wireguard.generateKeypair();
|
|
form.setFieldValue(['settings', 'secretKey'], pair.privateKey);
|
|
form.setFieldValue(['settings', 'pubKey'], pair.publicKey);
|
|
}}
|
|
/>
|
|
</>
|
|
}
|
|
name={['settings', 'secretKey']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item label={t('pages.inbounds.publicKey')} name={['settings', 'pubKey']}>
|
|
<Input disabled />
|
|
</Form.Item>
|
|
<Form.Item label="Domain strategy" name={['settings', 'domainStrategy']}>
|
|
<Select
|
|
options={[
|
|
{ value: '', label: `(${t('none')})` },
|
|
...WireguardDomainStrategy.map((s) => ({ value: s, label: s })),
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item label="MTU" name={['settings', 'mtu']}>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item label="Workers" name={['settings', 'workers']}>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="No-kernel TUN"
|
|
name={['settings', 'noKernelTun']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item label="Reserved" name={['settings', 'reserved']}>
|
|
<Input placeholder="comma-separated bytes, e.g. 1,2,3" />
|
|
</Form.Item>
|
|
<Form.List name={['settings', 'peers']}>
|
|
{(fields, { add, remove }) => (
|
|
<>
|
|
<Form.Item label="Peers">
|
|
<Button
|
|
size="small"
|
|
type="primary"
|
|
icon={<PlusOutlined />}
|
|
onClick={() =>
|
|
add({
|
|
publicKey: '',
|
|
psk: '',
|
|
allowedIPs: ['0.0.0.0/0', '::/0'],
|
|
endpoint: '',
|
|
keepAlive: 0,
|
|
})
|
|
}
|
|
/>
|
|
</Form.Item>
|
|
{fields.map((field, index) => (
|
|
<div key={field.key}>
|
|
<Form.Item wrapperCol={{ md: { span: 14, offset: 8 } }}>
|
|
<div className="item-heading">
|
|
<span>Peer {index + 1}</span>
|
|
{fields.length > 1 && (
|
|
<DeleteOutlined
|
|
className="danger-icon"
|
|
onClick={() => remove(field.name)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</Form.Item>
|
|
<Form.Item label="Endpoint" name={[field.name, 'endpoint']}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('pages.inbounds.publicKey')}
|
|
name={[field.name, 'publicKey']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item label="PSK" name={[field.name, 'psk']}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item label="Allowed IPs">
|
|
<Form.List name={[field.name, 'allowedIPs']}>
|
|
{(ipFields, { add: addIp, remove: removeIp }) => (
|
|
<>
|
|
{ipFields.map((ipField, ipIdx) => (
|
|
<Space.Compact
|
|
key={ipField.key}
|
|
block
|
|
style={{ marginBottom: 4 }}
|
|
>
|
|
<Form.Item noStyle name={ipField.name}>
|
|
<Input />
|
|
</Form.Item>
|
|
{ipFields.length > 1 && (
|
|
<InputAddon onClick={() => removeIp(ipIdx)}>
|
|
<MinusOutlined />
|
|
</InputAddon>
|
|
)}
|
|
</Space.Compact>
|
|
))}
|
|
<Button
|
|
size="small"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => addIp('')}
|
|
/>
|
|
</>
|
|
)}
|
|
</Form.List>
|
|
</Form.Item>
|
|
<Form.Item label="Keep alive" name={[field.name, 'keepAlive']}>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
</div>
|
|
))}
|
|
</>
|
|
)}
|
|
</Form.List>
|
|
</>
|
|
)}
|
|
|
|
{streamAllowed && network && (
|
|
<>
|
|
<Form.Item
|
|
label={t('transmission')}
|
|
name={['streamSettings', 'network']}
|
|
>
|
|
<Select
|
|
value={network}
|
|
onChange={onNetworkChange}
|
|
options={
|
|
protocol === 'hysteria'
|
|
? [...NETWORK_OPTIONS, HYSTERIA_NETWORK_OPTION]
|
|
: NETWORK_OPTIONS
|
|
}
|
|
/>
|
|
</Form.Item>
|
|
|
|
{network === 'tcp' && (
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
const type =
|
|
form.getFieldValue([
|
|
'streamSettings',
|
|
'tcpSettings',
|
|
'header',
|
|
'type',
|
|
]) ?? 'none';
|
|
return (
|
|
<>
|
|
<Form.Item label={`HTTP ${t('camouflage')}`}>
|
|
<Switch
|
|
checked={type === 'http'}
|
|
onChange={(checked) =>
|
|
form.setFieldValue(
|
|
['streamSettings', 'tcpSettings', 'header'],
|
|
checked
|
|
? {
|
|
type: 'http',
|
|
request: {
|
|
version: '1.1',
|
|
method: 'GET',
|
|
path: ['/'],
|
|
headers: {},
|
|
},
|
|
response: undefined,
|
|
}
|
|
: { type: 'none' },
|
|
)
|
|
}
|
|
/>
|
|
</Form.Item>
|
|
{type === 'http' && (
|
|
<>
|
|
{/* Host is stored as a string[] on the
|
|
wire (V2 header map: { Host: [...] }).
|
|
The form-level normalize/getValueProps
|
|
translate to/from a comma-joined input
|
|
so the user types one Host:contentReference[oaicite:0]{index=0} value per
|
|
server they want camouflaged. */}
|
|
<Form.Item
|
|
label={t('host')}
|
|
name={[
|
|
'streamSettings',
|
|
'tcpSettings',
|
|
'header',
|
|
'request',
|
|
'headers',
|
|
'Host',
|
|
]}
|
|
normalize={(v: unknown) =>
|
|
typeof v === 'string'
|
|
? v.split(',').map((s) => s.trim()).filter(Boolean)
|
|
: Array.isArray(v) ? v : []
|
|
}
|
|
getValueProps={(v: unknown) => ({
|
|
value: Array.isArray(v) ? v.join(',') : '',
|
|
})}
|
|
>
|
|
<Input placeholder="example.com,cdn.example.com" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('path')}
|
|
name={[
|
|
'streamSettings',
|
|
'tcpSettings',
|
|
'header',
|
|
'request',
|
|
'path',
|
|
]}
|
|
normalize={(v: unknown) =>
|
|
typeof v === 'string'
|
|
? v.split(',').map((s) => s.trim()).filter(Boolean)
|
|
: Array.isArray(v) ? v : ['/']
|
|
}
|
|
getValueProps={(v: unknown) => ({
|
|
value: Array.isArray(v) ? v.join(',') : '/',
|
|
})}
|
|
>
|
|
<Input placeholder="/,/api,/static" />
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
)}
|
|
|
|
{network === 'kcp' && (
|
|
<>
|
|
<Form.Item label="MTU" name={['streamSettings', 'kcpSettings', 'mtu']}>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item label="TTI (ms)" name={['streamSettings', 'kcpSettings', 'tti']}>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Uplink (MB/s)"
|
|
name={['streamSettings', 'kcpSettings', 'uplinkCapacity']}
|
|
>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Downlink (MB/s)"
|
|
name={['streamSettings', 'kcpSettings', 'downlinkCapacity']}
|
|
>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="CWND multiplier"
|
|
name={['streamSettings', 'kcpSettings', 'cwndMultiplier']}
|
|
>
|
|
<InputNumber min={1} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Max sending window"
|
|
name={['streamSettings', 'kcpSettings', 'maxSendingWindow']}
|
|
>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
|
|
{network === 'ws' && (
|
|
<>
|
|
<Form.Item label={t('host')} name={['streamSettings', 'wsSettings', 'host']}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item label={t('path')} name={['streamSettings', 'wsSettings', 'path']}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Heartbeat (s)"
|
|
name={['streamSettings', 'wsSettings', 'heartbeatPeriod']}
|
|
>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Headers"
|
|
name={['streamSettings', 'wsSettings', 'headers']}
|
|
>
|
|
<HeaderMapEditor mode="v1" />
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
|
|
{network === 'grpc' && (
|
|
<>
|
|
<Form.Item
|
|
label="Service name"
|
|
name={['streamSettings', 'grpcSettings', 'serviceName']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Authority"
|
|
name={['streamSettings', 'grpcSettings', 'authority']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Multi mode"
|
|
name={['streamSettings', 'grpcSettings', 'multiMode']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
|
|
{network === 'httpupgrade' && (
|
|
<>
|
|
<Form.Item
|
|
label={t('host')}
|
|
name={['streamSettings', 'httpupgradeSettings', 'host']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('path')}
|
|
name={['streamSettings', 'httpupgradeSettings', 'path']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Headers"
|
|
name={['streamSettings', 'httpupgradeSettings', 'headers']}
|
|
>
|
|
<HeaderMapEditor mode="v1" />
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
|
|
{network === 'xhttp' && (
|
|
<>
|
|
<Form.Item
|
|
label={t('host')}
|
|
name={['streamSettings', 'xhttpSettings', 'host']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('path')}
|
|
name={['streamSettings', 'xhttpSettings', 'path']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Mode"
|
|
name={['streamSettings', 'xhttpSettings', 'mode']}
|
|
>
|
|
<Select options={MODE_OPTIONS} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Padding Bytes"
|
|
name={['streamSettings', 'xhttpSettings', 'xPaddingBytes']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Headers"
|
|
name={['streamSettings', 'xhttpSettings', 'headers']}
|
|
>
|
|
<HeaderMapEditor mode="v1" />
|
|
</Form.Item>
|
|
|
|
{/* Padding obfs sub-section: gated by a Switch.
|
|
When on, four extra knobs (key/header/placement/
|
|
method) tune how Xray injects random padding to
|
|
disguise the post body shape. */}
|
|
<Form.Item
|
|
label="Padding obfs mode"
|
|
name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
const obfs = !!form.getFieldValue([
|
|
'streamSettings', 'xhttpSettings', 'xPaddingObfsMode',
|
|
]);
|
|
if (!obfs) return null;
|
|
return (
|
|
<>
|
|
<Form.Item
|
|
label="Padding key"
|
|
name={['streamSettings', 'xhttpSettings', 'xPaddingKey']}
|
|
>
|
|
<Input placeholder="x_padding" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Padding header"
|
|
name={['streamSettings', 'xhttpSettings', 'xPaddingHeader']}
|
|
>
|
|
<Input placeholder="X-Padding" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Padding placement"
|
|
name={['streamSettings', 'xhttpSettings', 'xPaddingPlacement']}
|
|
>
|
|
<Select
|
|
options={[
|
|
{ value: '', label: 'Default (queryInHeader)' },
|
|
{ value: 'queryInHeader', label: 'queryInHeader' },
|
|
{ value: 'header', label: 'header' },
|
|
{ value: 'cookie', label: 'cookie' },
|
|
{ value: 'query', label: 'query' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Padding method"
|
|
name={['streamSettings', 'xhttpSettings', 'xPaddingMethod']}
|
|
>
|
|
<Select
|
|
options={[
|
|
{ value: '', label: 'Default (repeat-x)' },
|
|
{ value: 'repeat-x', label: 'repeat-x' },
|
|
{ value: 'tokenish', label: 'tokenish' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
</>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="Uplink HTTP method"
|
|
name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
|
|
>
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
const mode = form.getFieldValue([
|
|
'streamSettings', 'xhttpSettings', 'mode',
|
|
]);
|
|
return (
|
|
<Select
|
|
options={[
|
|
{ value: '', label: 'Default (POST)' },
|
|
{ value: 'POST', label: 'POST' },
|
|
{ value: 'PUT', label: 'PUT' },
|
|
{ value: 'GET', label: 'GET (packet-up only)', disabled: mode !== 'packet-up' },
|
|
]}
|
|
/>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
</Form.Item>
|
|
|
|
{/* Session + sequence + uplinkData placements:
|
|
three orthogonal slots Xray uses to thread
|
|
request metadata through the transport
|
|
(path / header / cookie / query). Key field
|
|
only matters when placement is not 'path'. */}
|
|
<Form.Item
|
|
label="Session placement"
|
|
name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
|
|
>
|
|
<Select
|
|
options={[
|
|
{ value: '', label: 'Default (path)' },
|
|
{ value: 'path', label: 'path' },
|
|
{ value: 'header', label: 'header' },
|
|
{ value: 'cookie', label: 'cookie' },
|
|
{ value: 'query', label: 'query' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
const placement = form.getFieldValue([
|
|
'streamSettings', 'xhttpSettings', 'sessionPlacement',
|
|
]);
|
|
if (!placement || placement === 'path') return null;
|
|
return (
|
|
<Form.Item
|
|
label="Session key"
|
|
name={['streamSettings', 'xhttpSettings', 'sessionKey']}
|
|
>
|
|
<Input placeholder="x_session" />
|
|
</Form.Item>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Sequence placement"
|
|
name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
|
|
>
|
|
<Select
|
|
options={[
|
|
{ value: '', label: 'Default (path)' },
|
|
{ value: 'path', label: 'path' },
|
|
{ value: 'header', label: 'header' },
|
|
{ value: 'cookie', label: 'cookie' },
|
|
{ value: 'query', label: 'query' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
const placement = form.getFieldValue([
|
|
'streamSettings', 'xhttpSettings', 'seqPlacement',
|
|
]);
|
|
if (!placement || placement === 'path') return null;
|
|
return (
|
|
<Form.Item
|
|
label="Sequence key"
|
|
name={['streamSettings', 'xhttpSettings', 'seqKey']}
|
|
>
|
|
<Input placeholder="x_seq" />
|
|
</Form.Item>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
|
|
{/* Mode-conditional sub-sections. */}
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
const mode = form.getFieldValue([
|
|
'streamSettings', 'xhttpSettings', 'mode',
|
|
]);
|
|
if (mode !== 'packet-up') return null;
|
|
return (
|
|
<>
|
|
<Form.Item
|
|
label="Min upload interval (ms)"
|
|
name={['streamSettings', 'xhttpSettings', 'scMinPostsIntervalMs']}
|
|
>
|
|
<Input placeholder="30" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Max upload size (bytes)"
|
|
name={['streamSettings', 'xhttpSettings', 'scMaxEachPostBytes']}
|
|
>
|
|
<Input placeholder="1000000" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Uplink data placement"
|
|
name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']}
|
|
>
|
|
<Select
|
|
options={[
|
|
{ value: '', label: 'Default (body)' },
|
|
{ value: 'body', label: 'body' },
|
|
{ value: 'header', label: 'header' },
|
|
{ value: 'cookie', label: 'cookie' },
|
|
{ value: 'query', label: 'query' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
const place = form.getFieldValue([
|
|
'streamSettings', 'xhttpSettings', 'uplinkDataPlacement',
|
|
]);
|
|
if (!place || place === 'body') return null;
|
|
return (
|
|
<>
|
|
<Form.Item
|
|
label="Uplink data key"
|
|
name={['streamSettings', 'xhttpSettings', 'uplinkDataKey']}
|
|
>
|
|
<Input placeholder="x_data" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Uplink chunk size"
|
|
name={['streamSettings', 'xhttpSettings', 'uplinkChunkSize']}
|
|
>
|
|
<InputNumber
|
|
min={0}
|
|
placeholder="0 (unlimited)"
|
|
style={{ width: '100%' }}
|
|
/>
|
|
</Form.Item>
|
|
</>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
</>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
const mode = form.getFieldValue([
|
|
'streamSettings', 'xhttpSettings', 'mode',
|
|
]);
|
|
if (mode !== 'stream-up' && mode !== 'stream-one') return null;
|
|
return (
|
|
<Form.Item
|
|
label="No gRPC header"
|
|
name={['streamSettings', 'xhttpSettings', 'noGRPCHeader']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
|
|
{/* XMUX is the connection-multiplexing layer
|
|
xHTTP uses to fan out parallel requests over
|
|
a small pool of upstream connections. UI-only
|
|
toggle (enableXmux) hides the 6 nested knobs
|
|
when off. */}
|
|
<Form.Item
|
|
label="XMUX"
|
|
name={['streamSettings', 'xhttpSettings', 'enableXmux']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
if (!form.getFieldValue([
|
|
'streamSettings', 'xhttpSettings', 'enableXmux',
|
|
])) return null;
|
|
return (
|
|
<>
|
|
<Form.Item
|
|
label="Max concurrency"
|
|
name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConcurrency']}
|
|
>
|
|
<Input placeholder="16-32" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Max connections"
|
|
name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConnections']}
|
|
>
|
|
<Input placeholder="0" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Max reuse times"
|
|
name={['streamSettings', 'xhttpSettings', 'xmux', 'cMaxReuseTimes']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Max request times"
|
|
name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxRequestTimes']}
|
|
>
|
|
<Input placeholder="600-900" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Max reusable secs"
|
|
name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxReusableSecs']}
|
|
>
|
|
<Input placeholder="1800-3000" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Keep alive period"
|
|
name={['streamSettings', 'xhttpSettings', 'xmux', 'hKeepAlivePeriod']}
|
|
>
|
|
<InputNumber min={0} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
</>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
|
|
{network === 'hysteria' && (
|
|
<>
|
|
<Form.Item
|
|
label="Auth password"
|
|
name={['streamSettings', 'hysteriaSettings', 'auth']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Congestion"
|
|
name={['streamSettings', 'hysteriaSettings', 'congestion']}
|
|
>
|
|
<Select
|
|
options={[
|
|
{ value: '', label: 'BBR (auto)' },
|
|
{ value: 'brutal', label: 'Brutal' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Upload"
|
|
name={['streamSettings', 'hysteriaSettings', 'up']}
|
|
>
|
|
<Input placeholder="100 mbps" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Download"
|
|
name={['streamSettings', 'hysteriaSettings', 'down']}
|
|
>
|
|
<Input placeholder="100 mbps" />
|
|
</Form.Item>
|
|
<Form.Item label="UDP hop">
|
|
<Form.Item
|
|
shouldUpdate
|
|
noStyle
|
|
>
|
|
{() => {
|
|
const udphop = form.getFieldValue([
|
|
'streamSettings', 'hysteriaSettings', 'udphop',
|
|
]) as { port?: string } | undefined;
|
|
return (
|
|
<Switch
|
|
checked={!!udphop}
|
|
onChange={(checked) =>
|
|
form.setFieldValue(
|
|
['streamSettings', 'hysteriaSettings', 'udphop'],
|
|
checked
|
|
? { port: '', intervalMin: 30, intervalMax: 30 }
|
|
: undefined,
|
|
)
|
|
}
|
|
/>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
</Form.Item>
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
const udphop = form.getFieldValue([
|
|
'streamSettings', 'hysteriaSettings', 'udphop',
|
|
]) as { port?: string } | undefined;
|
|
if (!udphop) return null;
|
|
return (
|
|
<>
|
|
<Form.Item
|
|
label="UDP hop port"
|
|
name={['streamSettings', 'hysteriaSettings', 'udphop', 'port']}
|
|
>
|
|
<Input placeholder="1145-1919" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="UDP hop interval min (s)"
|
|
name={[
|
|
'streamSettings', 'hysteriaSettings',
|
|
'udphop', 'intervalMin',
|
|
]}
|
|
>
|
|
<InputNumber min={1} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="UDP hop interval max (s)"
|
|
name={[
|
|
'streamSettings', 'hysteriaSettings',
|
|
'udphop', 'intervalMax',
|
|
]}
|
|
>
|
|
<InputNumber min={1} />
|
|
</Form.Item>
|
|
</>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Max idle (s)"
|
|
name={['streamSettings', 'hysteriaSettings', 'maxIdleTimeout']}
|
|
>
|
|
<InputNumber min={1} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Keep alive (s)"
|
|
name={['streamSettings', 'hysteriaSettings', 'keepAlivePeriod']}
|
|
>
|
|
<InputNumber min={1} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Disable Path MTU"
|
|
name={['streamSettings', 'hysteriaSettings', 'disablePathMTUDiscovery']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<div style={{ marginTop: 4, opacity: 0.6, fontStyle: 'italic' }}>
|
|
Receive-window tuning (init/maxStreamReceiveWindow,
|
|
init/maxConnectionReceiveWindow) is rarely changed
|
|
— edit via the JSON tab if needed.
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{tlsFlowAllowed && (
|
|
<Form.Item label="Flow" name={['settings', 'flow']}>
|
|
<Select
|
|
allowClear
|
|
placeholder={t('none')}
|
|
options={FLOW_OPTIONS}
|
|
/>
|
|
</Form.Item>
|
|
)}
|
|
|
|
{/* Vision seed knobs only meaningful for the exact
|
|
xtls-rprx-vision flow, on TCP+(tls|reality). The
|
|
legacy class gated this on `canEnableVisionSeed()`
|
|
— same condition encoded inline here. */}
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
const flow =
|
|
(form.getFieldValue(['settings', 'flow']) ?? '') as string;
|
|
if (!(tlsFlowAllowed && flow === 'xtls-rprx-vision')) return null;
|
|
return (
|
|
<>
|
|
<Form.Item label="Vision testpre" name={['settings', 'testpre']}>
|
|
<InputNumber min={0} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Vision testseed"
|
|
name={['settings', 'testseed']}
|
|
normalize={(v: unknown) =>
|
|
Array.isArray(v)
|
|
? v
|
|
.map((x) => Number(x))
|
|
.filter((n) => Number.isInteger(n) && n > 0)
|
|
: []
|
|
}
|
|
>
|
|
<Select
|
|
mode="tags"
|
|
tokenSeparators={[',', ' ']}
|
|
placeholder="four positive integers"
|
|
/>
|
|
</Form.Item>
|
|
</>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
|
|
{streamAllowed && network && (
|
|
<Form.Item label={t('security')}>
|
|
<Radio.Group
|
|
value={security}
|
|
buttonStyle="solid"
|
|
onChange={(e) => onSecurityChange(e.target.value as string)}
|
|
>
|
|
<Radio.Button value="none">{t('none')}</Radio.Button>
|
|
{tlsAllowed && <Radio.Button value="tls">TLS</Radio.Button>}
|
|
{realityAllowed && <Radio.Button value="reality">Reality</Radio.Button>}
|
|
</Radio.Group>
|
|
</Form.Item>
|
|
)}
|
|
|
|
{security === 'tls' && tlsAllowed && (
|
|
<>
|
|
<Form.Item
|
|
label="SNI"
|
|
name={['streamSettings', 'tlsSettings', 'serverName']}
|
|
>
|
|
<Input placeholder="server name" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="uTLS"
|
|
name={['streamSettings', 'tlsSettings', 'fingerprint']}
|
|
>
|
|
<Select
|
|
allowClear
|
|
placeholder={t('none')}
|
|
options={UTLS_OPTIONS}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="ALPN"
|
|
name={['streamSettings', 'tlsSettings', 'alpn']}
|
|
>
|
|
<Select mode="multiple" options={ALPN_OPTIONS} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="ECH"
|
|
name={['streamSettings', 'tlsSettings', 'echConfigList']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Verify peer name"
|
|
name={['streamSettings', 'tlsSettings', 'verifyPeerCertByName']}
|
|
>
|
|
<Input placeholder="cloudflare-dns.com" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Pinned SHA256"
|
|
name={['streamSettings', 'tlsSettings', 'pinnedPeerCertSha256']}
|
|
>
|
|
<Input placeholder="base64 SHA256" />
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
|
|
{security === 'reality' && realityAllowed && (
|
|
<>
|
|
<Form.Item
|
|
label="SNI"
|
|
name={['streamSettings', 'realitySettings', 'serverName']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="uTLS"
|
|
name={['streamSettings', 'realitySettings', 'fingerprint']}
|
|
>
|
|
<Select options={UTLS_OPTIONS} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Short ID"
|
|
name={['streamSettings', 'realitySettings', 'shortId']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="SpiderX"
|
|
name={['streamSettings', 'realitySettings', 'spiderX']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('pages.inbounds.publicKey')}
|
|
name={['streamSettings', 'realitySettings', 'publicKey']}
|
|
>
|
|
<Input.TextArea autoSize={{ minRows: 2 }} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="mldsa65 verify"
|
|
name={['streamSettings', 'realitySettings', 'mldsa65Verify']}
|
|
>
|
|
<Input.TextArea autoSize={{ minRows: 2 }} />
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
|
|
{streamAllowed && network && (
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
const hasSockopt = !!form.getFieldValue([
|
|
'streamSettings',
|
|
'sockopt',
|
|
]);
|
|
return (
|
|
<>
|
|
<Form.Item label="Sockopts">
|
|
<Switch
|
|
checked={hasSockopt}
|
|
onChange={(checked) => {
|
|
form.setFieldValue(
|
|
['streamSettings', 'sockopt'],
|
|
checked
|
|
? {
|
|
acceptProxyProtocol: false,
|
|
tcpFastOpen: false,
|
|
mark: 0,
|
|
tproxy: 'off',
|
|
tcpMptcp: false,
|
|
penetrate: false,
|
|
domainStrategy: 'UseIP',
|
|
tcpMaxSeg: 1440,
|
|
dialerProxy: '',
|
|
tcpKeepAliveInterval: 0,
|
|
tcpKeepAliveIdle: 300,
|
|
tcpUserTimeout: 10000,
|
|
tcpcongestion: 'bbr',
|
|
V6Only: false,
|
|
tcpWindowClamp: 600,
|
|
interfaceName: '',
|
|
trustedXForwardedFor: [],
|
|
}
|
|
: undefined,
|
|
);
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
{hasSockopt && (
|
|
<>
|
|
<Form.Item
|
|
label="Dialer proxy"
|
|
name={['streamSettings', 'sockopt', 'dialerProxy']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Domain strategy"
|
|
name={['streamSettings', 'sockopt', 'domainStrategy']}
|
|
>
|
|
<Select
|
|
options={ADDRESS_PORT_STRATEGY_OPTIONS}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Keep alive interval"
|
|
name={['streamSettings', 'sockopt', 'tcpKeepAliveInterval']}
|
|
>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="TCP Fast Open"
|
|
name={['streamSettings', 'sockopt', 'tcpFastOpen']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Multipath TCP"
|
|
name={['streamSettings', 'sockopt', 'tcpMptcp']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Penetrate"
|
|
name={['streamSettings', 'sockopt', 'penetrate']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Mark (fwmark)"
|
|
name={['streamSettings', 'sockopt', 'mark']}
|
|
>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Interface"
|
|
name={['streamSettings', 'sockopt', 'interfaceName']}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="TProxy"
|
|
name={['streamSettings', 'sockopt', 'tproxy']}
|
|
>
|
|
<Select
|
|
options={[
|
|
{ value: 'off', label: 'off' },
|
|
{ value: 'redirect', label: 'redirect' },
|
|
{ value: 'tproxy', label: 'tproxy' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="TCP congestion"
|
|
name={['streamSettings', 'sockopt', 'tcpcongestion']}
|
|
>
|
|
<Select
|
|
options={Object.values(TCP_CONGESTION_OPTION).map((v) => ({
|
|
value: v,
|
|
label: v,
|
|
}))}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="IPv6 only"
|
|
name={['streamSettings', 'sockopt', 'V6Only']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="Accept proxy protocol"
|
|
name={['streamSettings', 'sockopt', 'acceptProxyProtocol']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="TCP user timeout (ms)"
|
|
name={['streamSettings', 'sockopt', 'tcpUserTimeout']}
|
|
>
|
|
<InputNumber min={0} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
)}
|
|
|
|
{(() => {
|
|
const flow = (form.getFieldValue(['settings', 'flow']) ?? '') as string;
|
|
if (!isMuxAllowed(protocol, flow, network)) return null;
|
|
return (
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
const muxEnabled = !!form.getFieldValue(['mux', 'enabled']);
|
|
return (
|
|
<>
|
|
<Form.Item
|
|
label={t('pages.settings.mux')}
|
|
name={['mux', 'enabled']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
{muxEnabled && (
|
|
<>
|
|
<Form.Item
|
|
label="Concurrency"
|
|
name={['mux', 'concurrency']}
|
|
>
|
|
<InputNumber min={-1} max={1024} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="xudp concurrency"
|
|
name={['mux', 'xudpConcurrency']}
|
|
>
|
|
<InputNumber min={-1} max={1024} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="xudp UDP 443"
|
|
name={['mux', 'xudpProxyUDP443']}
|
|
>
|
|
<Select
|
|
options={['reject', 'allow', 'skip'].map((v) => ({
|
|
value: v,
|
|
label: v,
|
|
}))}
|
|
/>
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
);
|
|
})()}
|
|
</>
|
|
),
|
|
},
|
|
{
|
|
key: '2',
|
|
label: 'JSON',
|
|
children: (
|
|
<Space orientation="vertical" size={10} style={{ width: '100%', marginTop: 10 }}>
|
|
<JsonEditor
|
|
value={jsonText}
|
|
onChange={(next) => {
|
|
setJsonText(next);
|
|
setJsonDirty(true);
|
|
}}
|
|
minHeight="360px"
|
|
maxHeight="600px"
|
|
/>
|
|
</Space>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</Form>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|