From 1aef7171e3368b01dca83c0d54b499dad5762ccb Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 26 May 2026 11:41:10 +0200 Subject: [PATCH] feat(frontend): atomic swap InboundFormModal to Pattern A MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deletes the 2261-line class-mutation modal and renames the 1900-line sibling rewrite into its place. InboundsPage.tsx already imports the file by path so no consumer change is needed — the swap is one file delete plus one file rename. Build, lint, and 280 tests stay green. What the new modal covers end-to-end: - Basic (enable / remark / nodeId / protocol / listen / port / totalGB / trafficReset / expireDate) - Sniffing (enabled / destOverride / metadataOnly / routeOnly / ipsExcluded / domainsExcluded) - Protocol per DU branch: VLESS (decryption/encryption + buttons), Shadowsocks (method/password/network/ivCheck), HTTP + Mixed (accounts list + per-protocol toggles), Tunnel (rewrite + portMap + followRedirect), TUN (interface/mtu + four primitive lists + userLevel/autoInterface), Wireguard (secretKey + derived pubKey + peers list with nested allowedIPs) - Stream per network: TCP base, KCP, WS, gRPC, HTTPUpgrade, XHTTP (the 22-field one), plus external-proxy and sockopt extras - Security: TLS (SNI/cipher/version/uTLS/ALPN/policy switches + certificates list with file/inline toggle + ECH controls), Reality (every field + the four API-call buttons), none - Advanced JSON (settings / streamSettings / sniffing live editors that round-trip into form state on every valid parse) - Fallbacks (load on open for VLESS/Trojan TLS-or-Reality TCP hosts; save through the secondary endpoint after the main POST succeeds) Known regressions vs the legacy modal, all reachable via Advanced JSON until backfilled in follow-up commits: - Hysteria stream sub-form (masquerade / udpIdleTimeout / version) — schema gap; the existing inbound DU has no hysteria stream branch - FinalMaskForm hookup — the component is still class-shape coupled - HeaderMapEditor — TCP request/response headers, WS / HTTPUpgrade / XHTTP headers, Hysteria masquerade headers all need a shared editor - TCP HTTP camouflage request/response body (version, method, path list, headers, status, reason) — only the on/off toggle is wired - Fallbacks polish — up/down move, quick-add-all, rederive-from-child, the per-row advanced-toggle / proxy-tag chips No reference to @/models/inbound's Inbound class anywhere in the new modal — only @/models/dbinbound (out of scope) and @/models/reality-targets (out of scope). The protocol-capabilities predicates and the rawInboundToFormValues + formValuesToWirePayload adapters carry every behavior the class used to provide. --- .../pages/inbounds/InboundFormModal.new.tsx | 2215 ---------- .../src/pages/inbounds/InboundFormModal.tsx | 3924 ++++++++--------- 2 files changed, 1939 insertions(+), 4200 deletions(-) delete mode 100644 frontend/src/pages/inbounds/InboundFormModal.new.tsx diff --git a/frontend/src/pages/inbounds/InboundFormModal.new.tsx b/frontend/src/pages/inbounds/InboundFormModal.new.tsx deleted file mode 100644 index 5fe915ef..00000000 --- a/frontend/src/pages/inbounds/InboundFormModal.new.tsx +++ /dev/null @@ -1,2215 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import dayjs from 'dayjs'; -import { - Button, - Card, - Checkbox, - Empty, - Form, - Input, - InputNumber, - Modal, - Radio, - Select, - Space, - Switch, - Tabs, - Tooltip, - Typography, - message, -} from 'antd'; -import { DeleteOutlined, MinusOutlined, PlusOutlined, SyncOutlined } from '@ant-design/icons'; - -import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils'; -import { - rawInboundToFormValues, - formValuesToWirePayload, -} from '@/lib/xray/inbound-form-adapter'; -import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults'; -import { - canEnableReality, - canEnableStream, - canEnableTls, - isSS2022, -} from '@/lib/xray/protocol-capabilities'; -import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks'; -import { getRandomRealityTarget } from '@/models/reality-targets'; -import { - InboundFormBaseSchema, - InboundFormSchema, - type FallbackRow, - type InboundFormValues, -} from '@/schemas/forms/inbound-form'; -import { antdRule } from '@/utils/zodForm'; -import { - ALPN_OPTION, - DOMAIN_STRATEGY_OPTION, - Protocols, - SNIFFING_OPTION, - TCP_CONGESTION_OPTION, - TLS_CIPHER_OPTION, - TLS_VERSION_OPTION, - USAGE_OPTION, - UTLS_FINGERPRINT, -} from '@/schemas/primitives'; -import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt'; -import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls'; -import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality'; -import DateTimePicker from '@/components/DateTimePicker'; -import InputAddon from '@/components/InputAddon'; -import JsonEditor from '@/components/JsonEditor'; -import type { FormInstance } from 'antd'; -import type { NamePath } from 'antd/es/form/interface'; - -const { TextArea } = Input; -import type { DBInbound } from '@/models/dbinbound'; -import type { NodeRecord } from '@/api/queries/useNodesQuery'; - -// Pattern A rewrite of InboundFormModal. Built as a sibling file so the -// build stays green while the rewrite progresses section by section. -// InboundsPage continues to render the old InboundFormModal.tsx until the -// atomic swap at the end (Core Decision 7). - -const { Text } = Typography; - -// Sub-editor for one slice of the form (settings, streamSettings, sniffing). -// Holds a local text buffer so the user can type freely; on every keystroke -// we try to JSON.parse and forward the result to form state. Invalid JSON -// is held in the buffer until the next valid moment — no panic on partial -// input. The buffer seeds once on mount; the modal's destroyOnHidden makes -// each open a fresh editor instance, so we don't need to re-sync on outer -// form changes. -function AdvancedSliceEditor({ - form, - path, - minHeight, - maxHeight, -}: { - form: FormInstance; - path: NamePath; - minHeight?: string; - maxHeight?: string; -}) { - const [text, setText] = useState(() => - JSON.stringify(form.getFieldValue(path) ?? {}, null, 2), - ); - return ( - { - setText(next); - try { - form.setFieldValue(path, JSON.parse(next)); - } catch { - - } - }} - /> - ); -} - -const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p })); -const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const; -const NODE_ELIGIBLE_PROTOCOLS = new Set([ - Protocols.VLESS, - Protocols.VMESS, - Protocols.TROJAN, - Protocols.SHADOWSOCKS, - Protocols.HYSTERIA, - Protocols.WIREGUARD, -]); - -interface InboundFormModalProps { - open: boolean; - onClose: () => void; - onSaved: () => void; - mode: 'add' | 'edit'; - dbInbound: DBInbound | null; - dbInbounds: DBInbound[]; - availableNodes?: NodeRecord[]; -} - -function buildAddModeValues(): InboundFormValues { - const settings = createDefaultInboundSettings('vless') ?? undefined; - return rawInboundToFormValues({ - protocol: 'vless', - settings, - streamSettings: { network: 'tcp', security: 'none' }, - sniffing: {}, - port: RandomUtil.randomInteger(10000, 60000), - listen: '', - tag: '', - enable: true, - trafficReset: 'never', - }); -} - -export default function InboundFormModalNew({ - open, - onClose, - onSaved, - mode, - dbInbound, - dbInbounds, - availableNodes, -}: InboundFormModalProps) { - const { t } = useTranslation(); - const [messageApi, messageContextHolder] = message.useMessage(); - const [form] = Form.useForm(); - const [saving, setSaving] = useState(false); - const fallbackKeyRef = useRef(0); - const [fallbacks, setFallbacks] = useState([]); - - const selectableNodes = (availableNodes || []).filter((n) => n.enable); - const protocol = (Form.useWatch('protocol', form) ?? '') as string; - const isNodeEligible = NODE_ELIGIBLE_PROTOCOLS.has(protocol); - const sniffingEnabled = Form.useWatch(['sniffing', 'enabled'], form) ?? false; - const vlessEncryption = Form.useWatch(['settings', 'encryption'], form) ?? ''; - const ssMethod = Form.useWatch(['settings', 'method'], form); - const isSSWith2022 = isSS2022({ - protocol, - settings: typeof ssMethod === 'string' ? { method: ssMethod } : {}, - }); - const mixedUdpOn = Form.useWatch(['settings', 'udp'], form) ?? false; - const network = Form.useWatch(['streamSettings', 'network'], form) ?? ''; - const security = Form.useWatch(['streamSettings', 'security'], form) ?? 'none'; - const streamEnabled = canEnableStream({ protocol }); - const tlsAllowed = canEnableTls({ protocol, streamSettings: { network, security } }); - const realityAllowed = canEnableReality({ protocol, streamSettings: { network, security } }); - const isFallbackHost = - (protocol === Protocols.VLESS || protocol === Protocols.TROJAN) - && network === 'tcp' - && (security === 'tls' || security === 'reality'); - - const fallbackChildOptions = (dbInbounds || []) - .filter((ib) => ib.id !== dbInbound?.id) - .map((ib) => ({ - label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`, - value: ib.id, - })); - - const loadFallbacks = async (masterId: number | null) => { - if (!masterId) { - setFallbacks([]); - return; - } - const msg = await HttpUtil.get(`/panel/api/inbounds/${masterId}/fallbacks`); - if (!msg?.success || !Array.isArray(msg.obj)) { - setFallbacks([]); - return; - } - setFallbacks( - (msg.obj as { childId: number; name?: string; alpn?: string; path?: string; xver?: number }[]) - .map((r) => ({ - rowKey: `fb-${++fallbackKeyRef.current}`, - childId: r.childId, - name: r.name || '', - alpn: r.alpn || '', - path: r.path || '', - xver: r.xver || 0, - })), - ); - }; - - const saveFallbacks = async (masterId: number) => { - if (!masterId) return true; - const payload = { - fallbacks: fallbacks.filter((c) => c.childId).map((c, i) => ({ - childId: c.childId, - name: c.name, - alpn: c.alpn, - path: c.path, - xver: Number(c.xver) || 0, - sortOrder: i, - })), - }; - const msg = await HttpUtil.post( - `/panel/api/inbounds/${masterId}/fallbacks`, - payload, - { headers: { 'Content-Type': 'application/json' } }, - ); - return !!msg?.success; - }; - - const addFallback = () => { - setFallbacks((prev) => [...prev, { - rowKey: `fb-${++fallbackKeyRef.current}`, - childId: null, - name: '', - alpn: '', - path: '', - xver: 0, - }]); - }; - - const updateFallback = (rowKey: string, patch: Partial) => { - setFallbacks((prev) => prev.map((r) => r.rowKey === rowKey ? { ...r, ...patch } : r)); - }; - - const removeFallback = (idx: number) => { - setFallbacks((prev) => prev.filter((_, i) => i !== idx)); - }; - - const genRealityKeypair = async () => { - setSaving(true); - try { - const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert'); - if (msg?.success) { - const obj = msg.obj as { privateKey: string; publicKey: string }; - form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], obj.privateKey); - form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], obj.publicKey); - } - } finally { - setSaving(false); - } - }; - - const clearRealityKeypair = () => { - form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], ''); - form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], ''); - }; - - const genMldsa65 = async () => { - setSaving(true); - try { - const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65'); - if (msg?.success) { - const obj = msg.obj as { seed: string; verify: string }; - form.setFieldValue(['streamSettings', 'realitySettings', 'mldsa65Seed'], obj.seed); - form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify'], obj.verify); - } - } finally { - setSaving(false); - } - }; - - const clearMldsa65 = () => { - form.setFieldValue(['streamSettings', 'realitySettings', 'mldsa65Seed'], ''); - form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify'], ''); - }; - - const randomizeRealityTarget = () => { - const tgt = getRandomRealityTarget() as { target: string; sni: string }; - form.setFieldValue(['streamSettings', 'realitySettings', 'target'], tgt.target); - form.setFieldValue( - ['streamSettings', 'realitySettings', 'serverNames'], - tgt.sni.split(',').map((s) => s.trim()).filter(Boolean), - ); - }; - - const randomizeShortIds = () => { - form.setFieldValue( - ['streamSettings', 'realitySettings', 'shortIds'], - RandomUtil.randomShortIds().split(',').map((s) => s.trim()).filter(Boolean), - ); - }; - - const getNewEchCert = async () => { - const sni = form.getFieldValue(['streamSettings', 'tlsSettings', 'serverName']); - setSaving(true); - try { - const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni }); - if (msg?.success) { - const obj = msg.obj as { echServerKeys: string; echConfigList: string }; - form.setFieldValue(['streamSettings', 'tlsSettings', 'echServerKeys'], obj.echServerKeys); - form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], obj.echConfigList); - } - } finally { - setSaving(false); - } - }; - - const clearEchCert = () => { - form.setFieldValue(['streamSettings', 'tlsSettings', 'echServerKeys'], ''); - form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], ''); - }; - - const onSecurityChange = (next: string) => { - const current = (form.getFieldValue('streamSettings') as Record) ?? {}; - const cleaned: Record = { ...current, security: next }; - delete cleaned.tlsSettings; - delete cleaned.realitySettings; - if (next === 'tls') cleaned.tlsSettings = TlsStreamSettingsSchema.parse({}); - if (next === 'reality') cleaned.realitySettings = RealityStreamSettingsSchema.parse({}); - form.setFieldValue('streamSettings', cleaned); - }; - const xhttpMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'mode'], form); - const xhttpObfsMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'xPaddingObfsMode'], form) ?? false; - const xhttpSessionPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'sessionPlacement'], form); - const xhttpSeqPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'seqPlacement'], form); - const xhttpUplinkPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'uplinkDataPlacement'], form); - const externalProxyArr = Form.useWatch(['streamSettings', 'externalProxy'], form); - const externalProxyOn = Array.isArray(externalProxyArr) && externalProxyArr.length > 0; - const sockoptValue = Form.useWatch(['streamSettings', 'sockopt'], form); - const sockoptOn = !!sockoptValue && typeof sockoptValue === 'object' && Object.keys(sockoptValue as object).length > 0; - - const toggleExternalProxy = (on: boolean) => { - if (on) { - const port = (form.getFieldValue('port') as number) ?? 443; - form.setFieldValue(['streamSettings', 'externalProxy'], [{ - forceTls: 'same', - dest: typeof window !== 'undefined' ? window.location.hostname : '', - port, - remark: '', - sni: '', - fingerprint: '', - alpn: [], - }]); - } else { - form.setFieldValue(['streamSettings', 'externalProxy'], []); - } - }; - - const toggleSockopt = (on: boolean) => { - if (on) { - form.setFieldValue( - ['streamSettings', 'sockopt'], - SockoptStreamSettingsSchema.parse({}), - ); - } else { - form.setFieldValue(['streamSettings', 'sockopt'], undefined); - } - }; - const wgSecretKey = Form.useWatch(['settings', 'secretKey'], form); - const wgPubKey = typeof wgSecretKey === 'string' && wgSecretKey.length > 0 - ? Wireguard.generateKeypair(wgSecretKey).publicKey - : ''; - - const regenInboundWg = () => { - const kp = Wireguard.generateKeypair(); - form.setFieldValue(['settings', 'secretKey'], kp.privateKey); - }; - - const regenWgPeerKeypair = (peerName: number) => { - const kp = Wireguard.generateKeypair(); - form.setFieldValue(['settings', 'peers', peerName, 'privateKey'], kp.privateKey); - form.setFieldValue(['settings', 'peers', peerName, 'publicKey'], kp.publicKey); - }; - - const matchesVlessAuth = ( - block: { id?: string; label?: string } | undefined | null, - authId: string, - ) => { - if (block?.id === authId) return true; - const label = (block?.label || '').toLowerCase().replace(/[-_\s]/g, ''); - if (authId === 'mlkem768') return label.includes('mlkem768'); - if (authId === 'x25519') return label.includes('x25519'); - return false; - }; - - const getNewVlessEnc = async (authId: string) => { - if (!authId) return; - setSaving(true); - try { - const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc'); - if (!msg?.success) return; - const obj = msg.obj as { - auths?: { decryption: string; encryption: string; label?: string; id?: string }[]; - }; - const block = (obj.auths || []).find((a) => matchesVlessAuth(a, authId)); - if (!block) return; - form.setFieldValue(['settings', 'decryption'], block.decryption); - form.setFieldValue(['settings', 'encryption'], block.encryption); - } finally { - setSaving(false); - } - }; - - const clearVlessEnc = () => { - form.setFieldValue(['settings', 'decryption'], 'none'); - form.setFieldValue(['settings', 'encryption'], 'none'); - }; - - const selectedVlessAuth = (() => { - const enc = typeof vlessEncryption === 'string' ? vlessEncryption : ''; - if (!enc || enc === 'none') return 'None'; - const parts = enc.split('.').filter(Boolean); - const authKey = parts[parts.length - 1] || ''; - if (!authKey) return t('pages.inbounds.vlessAuthCustom'); - return authKey.length > 300 - ? t('pages.inbounds.vlessAuthMlkem768') - : t('pages.inbounds.vlessAuthX25519'); - })(); - - useEffect(() => { - if (!open) return; - const initial = mode === 'edit' && dbInbound - ? rawInboundToFormValues(dbInbound) - : buildAddModeValues(); - form.resetFields(); - form.setFieldsValue(initial); - if ( - mode === 'edit' - && dbInbound - && (dbInbound.protocol === Protocols.VLESS || dbInbound.protocol === Protocols.TROJAN) - ) { - loadFallbacks(dbInbound.id); - } else { - setFallbacks([]); - } - - }, [open, mode, dbInbound, form]); - - // Why: protocol picker reset cascades through the form — clearing the - // settings DU branch and dropping a nodeId that no longer applies. The - // legacy modal did this imperatively in onProtocolChange; here we hook - // into AntD's onValuesChange and let setFieldValue keep the rest of - // the form state intact. - const onValuesChange = (changed: Partial) => { - if (mode === 'edit') return; - if ('protocol' in changed && typeof changed.protocol === 'string') { - const next = changed.protocol; - const settings = createDefaultInboundSettings(next) ?? undefined; - form.setFieldValue('settings', settings); - if (!NODE_ELIGIBLE_PROTOCOLS.has(next)) { - form.setFieldValue('nodeId', null); - } - } - }; - - const submit = async () => { - let values: InboundFormValues; - try { - values = await form.validateFields(); - } catch { - return; - } - const parsed = InboundFormSchema.safeParse(values); - if (!parsed.success) { - const issue = parsed.error.issues[0]; - messageApi.error( - t(issue?.message ?? 'somethingWentWrong', { - defaultValue: issue?.message ?? 'invalid', - }), - ); - return; - } - setSaving(true); - try { - const payload = formValuesToWirePayload(parsed.data); - const url = mode === 'edit' && dbInbound - ? `/panel/api/inbounds/update/${dbInbound.id}` - : '/panel/api/inbounds/add'; - const msg = await HttpUtil.post(url, payload); - if (msg?.success) { - if (isFallbackHost) { - const obj = msg.obj as { id?: number; Id?: number } | null; - const masterId = mode === 'edit' - ? dbInbound!.id - : (obj?.id ?? obj?.Id ?? 0); - if (masterId) await saveFallbacks(masterId); - } - onSaved(); - onClose(); - } - } finally { - setSaving(false); - } - }; - - const title = mode === 'edit' - ? t('pages.inbounds.modifyInbound') - : t('pages.inbounds.addInbound'); - - const okText = mode === 'edit' - ? t('pages.clients.submitEdit') - : t('create'); - - const basicTab = ( - <> - - - - - - - - - {selectableNodes.length > 0 && isNodeEligible && ( - - - - )} - - - - - - - - - - - {t('pages.inbounds.totalFlow')} - - } - > - prev.total !== curr.total} - > - {({ getFieldValue, setFieldValue }) => { - const totalBytes = (getFieldValue('total') as number) ?? 0; - const totalGB = totalBytes - ? Math.round((totalBytes / SizeFormatter.ONE_GB) * 100) / 100 - : 0; - return ( - { - const bytes = NumberFormatter.toFixed( - (Number(v) || 0) * SizeFormatter.ONE_GB, - 0, - ); - setFieldValue('total', bytes); - }} - /> - ); - }} - - - - - - - - - {t('pages.inbounds.expireDate')} - - } - > - prev.expiryTime !== curr.expiryTime} - > - {({ getFieldValue, setFieldValue }) => { - const expiry = (getFieldValue('expiryTime') as number) ?? 0; - return ( - 0 ? dayjs(expiry) : null} - onChange={(d) => setFieldValue('expiryTime', d ? d.valueOf() : 0)} - /> - ); - }} - - - - ); - - const fallbacksCard = ( - - {fallbacks.length === 0 && ( - - )} - {fallbacks.map((record, idx) => ( -
- - updateFallback(record.rowKey, { name: e.target.value })} - /> - ALPN - updateFallback(record.rowKey, { alpn: e.target.value })} - /> - Path - updateFallback(record.rowKey, { path: e.target.value })} - /> - xver - updateFallback(record.rowKey, { xver: Number(v) || 0 })} - /> - -
- ))} - -
- ); - - const protocolTab = ( - <> - {protocol === Protocols.WIREGUARD && ( - <> - - Secret key{' '} - - - } - > - - - - - - - - - - - - - {(fields, { add, remove }) => ( - <> - - - - {fields.map((field, idx) => ( -
- - {fields.length > 1 && ( - - )} - - - Secret key{' '} - regenWgPeerKeypair(field.name)} - /> - - } - > - - - - - - - - - - {(ipFields, { add: addIp, remove: removeIp }) => ( - - - {ipFields.map((ipField) => ( - - - - - {ipFields.length > 1 && ( - - )} - - ))} - - )} - - - - -
- ))} - - )} -
- - )} - - {protocol === Protocols.TUN && ( - <> - - - - - - - - {(fields, { add, remove }) => ( - - - {fields.map((field, j) => ( - - - - - - - ))} - - )} - - - {(fields, { add, remove }) => ( - - - {fields.map((field, j) => ( - - - - - - - ))} - - )} - - - - - - {(fields, { add, remove }) => ( - - Auto system routes - - } - > - - {fields.map((field, j) => ( - - - - - - - ))} - - )} - - - Auto outbounds interface - - } - > - - - - )} - - {protocol === Protocols.TUNNEL && ( - <> - - - - - - - - - - - {(fields, { add, remove }) => ( - <> - - - - {fields.length > 0 && ( - - {fields.map((field, idx) => ( - - {String(idx + 1)} - - - - - - - - - ))} - - )} - - )} - - - - - - )} - - {(protocol === Protocols.HTTP || protocol === Protocols.MIXED) && ( - <> - - {(fields, { add, remove }) => ( - <> - - - - {fields.length > 0 && ( - - {fields.map((field, idx) => ( - - {String(idx + 1)} - - - - - - - - - ))} - - )} - - )} - - {protocol === Protocols.HTTP && ( - - - - )} - {protocol === Protocols.MIXED && ( - <> - - - - - - - {mixedUdpOn && ( - - - - )} - - )} - - )} - - {protocol === Protocols.SHADOWSOCKS && ( - <> - - - - {isSSWith2022 && ( - - Password{' '} - { - const method = form.getFieldValue(['settings', 'method']); - form.setFieldValue( - ['settings', 'password'], - RandomUtil.randomShadowsocksPassword(method as string), - ); - }} - /> - - } - > - - - )} - - - - - - - - )} - - {protocol === Protocols.VLESS && ( - <> - - - - - - - - - - - - - - {t('pages.inbounds.vlessAuthSelected', { auth: selectedVlessAuth })} - - - - )} - - {isFallbackHost && fallbacksCard} - - ); - - // Switching `network` swaps which per-network key (tcpSettings, wsSettings, - // grpcSettings, ...) appears on the wire. We clear the previously selected - // network's settings blob and seed a default empty object for the new one - // so AntD's Form.Items aren't pointed at undefined nested paths. - const onNetworkChange = (next: string) => { - const ALL = ['tcpSettings', 'kcpSettings', 'wsSettings', 'grpcSettings', 'httpupgradeSettings', 'xhttpSettings']; - const current = (form.getFieldValue('streamSettings') as Record) ?? {}; - const cleaned: Record = { ...current, network: next }; - for (const k of ALL) { - if (k !== `${next}Settings`) delete cleaned[k]; - } - cleaned[`${next}Settings`] = {}; - form.setFieldValue('streamSettings', cleaned); - }; - - const streamTab = ( - <> - {protocol !== Protocols.HYSTERIA && ( - - - - )} - - {network === 'tcp' && ( - <> - - - - - - prev.streamSettings?.tcpSettings?.header?.type - !== curr.streamSettings?.tcpSettings?.header?.type - } - > - {({ getFieldValue, setFieldValue }) => { - const headerType = getFieldValue( - ['streamSettings', 'tcpSettings', 'header', 'type'], - ) as string | undefined; - return ( - { - setFieldValue( - ['streamSettings', 'tcpSettings', 'header'], - v ? { type: 'http' } : { type: 'none' }, - ); - }} - /> - ); - }} - - - - )} - - {network === 'ws' && ( - <> - - - - - - - - - - - - - - )} - - {network === 'grpc' && ( - <> - - - - - - - - - - - )} - - {network === 'xhttp' && ( - <> - - - - - - - - - - {xhttpMode === 'packet-up' && ( - <> - - - - - - - - )} - {xhttpMode === 'stream-up' && ( - - - - )} - - - - - - - - - - - - - {xhttpObfsMode && ( - <> - - - - - - - - - - - - - - )} - - - - {xhttpSessionPlacement && xhttpSessionPlacement !== 'path' && ( - - - - )} - - - - {xhttpSeqPlacement && xhttpSeqPlacement !== 'path' && ( - - - - )} - {xhttpMode === 'packet-up' && ( - <> - - - - {xhttpUplinkPlacement && xhttpUplinkPlacement !== 'body' && ( - - - - )} - - )} - - - - - )} - - {network === 'httpupgrade' && ( - <> - - - - - - - - - - - )} - - {network === 'kcp' && ( - <> - - - - - - - - - - - - - - - - - - - - )} - - - - - {externalProxyOn && ( - - {(fields, { add, remove }) => ( - <> - - - - - {fields.map((field) => ( -
- - - - - - - - - - - - - - remove(field.name)}> - - - - - prev.streamSettings?.externalProxy?.[field.name]?.forceTls - !== curr.streamSettings?.externalProxy?.[field.name]?.forceTls - } - > - {({ getFieldValue }) => { - const ft = getFieldValue([ - 'streamSettings', 'externalProxy', field.name, 'forceTls', - ]); - if (ft !== 'tls') return null; - return ( - - - - - - - - - - - - ); - }} - -
- ))} -
- - )} -
- )} - - - - - {sockoptOn && ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )} - - ); - - const securityTab = ( - <> - - - prev.streamSettings?.security !== curr.streamSettings?.security - } - > - {({ getFieldValue }) => { - const sec = getFieldValue(['streamSettings', 'security']) ?? 'none'; - return ( - - ); - }} - - - - {security === 'tls' && ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {(certFields, { add, remove }) => ( - <> - - - - {certFields.map((certField, idx) => ( -
- - - - {t('pages.inbounds.certificatePath')} - - - {t('pages.inbounds.certificateContent')} - - - - {certFields.length > 1 && ( - - - - )} - - prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile - !== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile - } - > - {({ getFieldValue }) => { - const useFile = getFieldValue([ - 'streamSettings', 'tlsSettings', 'certificates', - certField.name, 'useFile', - ]); - return useFile ? ( - <> - - - - - - - - ) : ( - <> - typeof v === 'string' - ? v.split('\n') - : v} - getValueProps={(v) => ({ - value: Array.isArray(v) ? v.join('\n') : v, - })} - > -