import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import dayjs, { type Dayjs } from 'dayjs'; import { Button, Card, Checkbox, Col, Divider, Empty, Form, Input, InputNumber, Modal, Radio, Row, Select, Space, Switch, Tabs, Tooltip, Typography, message, } from 'antd'; import { SyncOutlined, PlusOutlined, MinusOutlined, DeleteOutlined, CaretUpOutlined, CaretDownOutlined, SettingOutlined, } from '@ant-design/icons'; import { HttpUtil, RandomUtil, NumberFormatter, SizeFormatter, Wireguard, } from '@/utils'; import InputAddon from '@/components/InputAddon'; import { getRandomRealityTarget } from '@/models/reality-targets'; import { Inbound, Protocols, SSMethods, SNIFFING_OPTION, TLS_VERSION_OPTION, TLS_CIPHER_OPTION, UTLS_FINGERPRINT, ALPN_OPTION, USAGE_OPTION, DOMAIN_STRATEGY_OPTION, TCP_CONGESTION_OPTION, MODE_OPTION, } from '@/models/inbound'; import { DBInbound } from '@/models/dbinbound'; import FinalMaskForm from '@/components/FinalMaskForm'; import DateTimePicker from '@/components/DateTimePicker'; import JsonEditor from '@/components/JsonEditor'; import type { NodeRecord } from '@/api/queries/useNodesQuery'; import { InboundFormSchema } from '@/schemas/inbound'; import './InboundFormModal.css'; const { TextArea } = Input; const { Text, Paragraph } = Typography; interface InboundFormModalProps { open: boolean; onClose: () => void; onSaved: () => void; mode: 'add' | 'edit'; dbInbound: DBInbound | null; dbInbounds: DBInbound[]; availableNodes?: NodeRecord[]; } interface StreamLike { network?: string; tcp?: { type?: string; request?: { path?: string[] }; acceptProxyProtocol?: boolean }; ws?: { path?: string; acceptProxyProtocol?: boolean }; grpc?: { serviceName?: string; multiMode?: boolean }; httpupgrade?: { path?: string; acceptProxyProtocol?: boolean }; xhttp?: { path?: string }; security?: string; tls?: { certs?: TlsCert[] }; reality?: unknown; externalProxy?: unknown; } interface TlsCert { useFile?: boolean; certFile?: string; keyFile?: string; cert?: string; key?: string; ocspStapling?: number; oneTimeLoading?: boolean; usage?: string; buildChain?: boolean; } interface VlessClient { id?: string; email?: string; flow?: string; enable?: boolean; subId?: string; totalGB?: number; expiryTime?: number; limitIp?: number; comment?: string; tgId?: string; } interface ShadowsocksClient { email?: string; password?: string; method?: string; enable?: boolean; subId?: string; totalGB?: number; expiryTime?: number; limitIp?: number; comment?: string; tgId?: string; } interface HttpAccount { user?: string; pass?: string; } interface WireguardPeer { privateKey?: string; publicKey?: string; psk?: string; allowedIPs: string[]; keepAlive?: number; } const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly']; const PROTOCOLS = Object.values(Protocols) as string[]; const TLS_VERSIONS = Object.values(TLS_VERSION_OPTION) as string[]; const CIPHER_SUITES = Object.entries(TLS_CIPHER_OPTION) as [string, string][]; const FINGERPRINTS = Object.values(UTLS_FINGERPRINT) as string[]; const ALPNS = Object.values(ALPN_OPTION) as string[]; const USAGES = Object.values(USAGE_OPTION) as string[]; const DOMAIN_STRATEGIES = Object.values(DOMAIN_STRATEGY_OPTION) as string[]; const TCP_CONGESTIONS = Object.values(TCP_CONGESTION_OPTION) as string[]; const MODE_OPTIONS = Object.values(MODE_OPTION) as string[]; const NODE_ELIGIBLE_PROTOCOLS = new Set([ Protocols.VLESS, Protocols.VMESS, Protocols.TROJAN, Protocols.SHADOWSOCKS, Protocols.HYSTERIA, Protocols.WIREGUARD, ]); const FALLBACK_ELIGIBLE_TRANSPORTS = new Set(['tcp', 'ws', 'grpc', 'httpupgrade', 'xhttp']); interface FallbackRow { rowKey: string; childId: number | null; name: string; alpn: string; path: string; xver: number; } function deriveFallbackDefaults(childDb: DBInbound | null | undefined): Omit { const out = { name: '', alpn: '', path: '', xver: 0 }; if (!childDb) return out; let stream: StreamLike | undefined; try { stream = childDb.toInbound()?.stream as StreamLike | undefined; } catch { return out; } if (!stream) return out; switch (stream.network) { case 'tcp': { const tcp = stream.tcp; if (tcp?.type === 'http') { const p = tcp?.request?.path; if (Array.isArray(p) && p.length) out.path = p[0]; } if (tcp?.acceptProxyProtocol) out.xver = 2; break; } case 'ws': { out.path = stream.ws?.path || ''; if (stream.ws?.acceptProxyProtocol) out.xver = 2; break; } case 'grpc': { out.path = stream.grpc?.serviceName || ''; out.alpn = 'h2'; break; } case 'httpupgrade': { out.path = stream.httpupgrade?.path || ''; if (stream.httpupgrade?.acceptProxyProtocol) out.xver = 2; break; } case 'xhttp': { out.path = stream.xhttp?.path || ''; break; } } return out; } export default function InboundFormModal({ open, onClose, onSaved, mode, dbInbound, dbInbounds, availableNodes, }: InboundFormModalProps) { const { t } = useTranslation(); const [messageApi, messageContextHolder] = message.useMessage(); const selectableNodes = useMemo( () => (availableNodes || []).filter((n: NodeRecord) => n.enable), [availableNodes], ); // eslint-disable-next-line @typescript-eslint/no-explicit-any const inboundRef = useRef(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any const dbFormRef = useRef(null); const fallbackKeyRef = useRef(0); const advancedTextRef = useRef({ stream: '', sniffing: '', settings: '' }); const [, setTick] = useState(0); const refresh = useCallback(() => setTick((n) => n + 1), []); const [saving, setSaving] = useState(false); const [activeTabKey, setActiveTabKey] = useState('basic'); const [advancedSectionKey, setAdvancedSectionKey] = useState('all'); const [defaultCert, setDefaultCert] = useState(''); const [defaultKey, setDefaultKey] = useState(''); const [fallbacks, setFallbacks] = useState([]); const [fallbackEditing, setFallbackEditing] = useState>(new Set()); const isVlessLike = inboundRef.current?.protocol === Protocols.VLESS; const isFallbackHost = useMemo(() => { const ib = inboundRef.current; if (!ib) return false; if (ib.protocol !== Protocols.VLESS && ib.protocol !== Protocols.TROJAN) return false; if (ib.stream?.network !== 'tcp') return false; const sec = ib.stream?.security; return sec === 'tls' || sec === 'reality'; // eslint-disable-next-line react-hooks/exhaustive-deps }, [inboundRef.current?.protocol, inboundRef.current?.stream?.network, inboundRef.current?.stream?.security]); const canEnableStream = inboundRef.current?.canEnableStream?.() === true; const canEnableTls = inboundRef.current?.canEnableTls?.() === true; const canEnableReality = inboundRef.current?.canEnableReality?.() === true; const isNodeEligible = NODE_ELIGIBLE_PROTOCOLS.has(inboundRef.current?.protocol); const hasProtocolTabContent = useMemo(() => { const ib = inboundRef.current; if (!ib) return false; if (ib.protocol === Protocols.VLESS) return true; if (isFallbackHost) return true; switch (ib.protocol) { case Protocols.SHADOWSOCKS: case Protocols.HTTP: case Protocols.MIXED: case Protocols.TUNNEL: case Protocols.TUN: case Protocols.WIREGUARD: return true; default: return false; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [inboundRef.current?.protocol, isFallbackHost]); const externalProxyOn = Array.isArray(inboundRef.current?.stream?.externalProxy) && inboundRef.current.stream.externalProxy.length > 0; const stampAdvancedTextFor = useCallback((slice: 'stream' | 'sniffing' | 'settings') => { const ib = inboundRef.current; if (!ib) return; if (slice === 'stream' && !ib.canEnableStream?.()) { advancedTextRef.current.stream = '{}'; return; } const obj = ib[slice]; if (!obj) return; try { advancedTextRef.current[slice] = JSON.stringify(JSON.parse(obj.toString()), null, 2); } catch { /* keep prior */ } }, []); const primeAdvancedJson = useCallback(() => { (['stream', 'sniffing', 'settings'] as const).forEach(stampAdvancedTextFor); }, [stampAdvancedTextFor]); const loadFallbacks = useCallback(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 fetchDefaultCertSettings = useCallback(async () => { try { const msg = await HttpUtil.post('/panel/setting/defaultSettings'); if (msg?.success && msg.obj) { const obj = msg.obj as { defaultCert?: string; defaultKey?: string }; setDefaultCert(obj.defaultCert || ''); setDefaultKey(obj.defaultKey || ''); } } catch { /* non-fatal */ } }, []); useEffect(() => { if (!open) return; setFallbackEditing(new Set()); if (mode === 'edit' && dbInbound) { const parsed = Inbound.fromJson(dbInbound.toInbound().toJson()); inboundRef.current = parsed; dbFormRef.current = new DBInbound(dbInbound); primeAdvancedJson(); if (dbInbound.protocol === Protocols.VLESS || dbInbound.protocol === Protocols.TROJAN) { loadFallbacks(dbInbound.id); } else { setFallbacks([]); } } else { const ib = new Inbound(); ib.protocol = Protocols.VLESS; ib.settings = Inbound.Settings.getSettings(Protocols.VLESS); ib.port = RandomUtil.randomInteger(10000, 60000); inboundRef.current = ib; const form = new DBInbound(); form.enable = true; form.remark = ''; form.total = 0; form.expiryTime = 0; form.trafficReset = 'never'; dbFormRef.current = form; primeAdvancedJson(); setFallbacks([]); } setActiveTabKey('basic'); setAdvancedSectionKey('all'); fetchDefaultCertSettings(); refresh(); }, [open, mode, dbInbound, primeAdvancedJson, loadFallbacks, fetchDefaultCertSettings, refresh]); const setExternalProxy = useCallback((on: boolean) => { const ib = inboundRef.current; if (!ib?.stream) return; if (on) { ib.stream.externalProxy = [{ forceTls: 'same', dest: window.location.hostname, port: ib.port, remark: '', sni: '', fingerprint: '', alpn: [], }]; } else { ib.stream.externalProxy = []; } refresh(); }, [refresh]); const onProtocolChange = useCallback((next: string) => { const ib = inboundRef.current; if (mode === 'edit' || !ib) return; ib.protocol = next; ib.settings = Inbound.Settings.getSettings(next); if (!NODE_ELIGIBLE_PROTOCOLS.has(next) && dbFormRef.current) { dbFormRef.current.nodeId = null; } primeAdvancedJson(); refresh(); }, [mode, primeAdvancedJson, refresh]); const onNetworkChange = useCallback((next: string) => { const ib = inboundRef.current; if (!ib?.stream) return; ib.stream.network = next; if (!ib.canEnableTls()) ib.stream.security = 'none'; if (!ib.canEnableReality()) ib.reality = false; if ( ib.protocol === Protocols.VLESS && !ib.canEnableTlsFlow() && Array.isArray(ib.settings.vlesses) ) { ib.settings.vlesses.forEach((c: VlessClient) => { c.flow = ''; }); } if (next !== 'kcp' && ib.stream.finalmask) { ib.stream.finalmask.udp = []; } stampAdvancedTextFor('stream'); refresh(); }, [stampAdvancedTextFor, refresh]); const setSecurity = useCallback((v: string) => { const ib = inboundRef.current; if (ib?.stream) { ib.stream.security = v; refresh(); } }, [refresh]); const addFallback = useCallback((childId: number | null = null) => { const row: FallbackRow = { rowKey: `fb-${++fallbackKeyRef.current}`, childId: childId || null, name: '', alpn: '', path: '', xver: 0, }; if (childId) { const child = (dbInbounds || []).find((ib) => ib.id === childId); Object.assign(row, deriveFallbackDefaults(child)); } setFallbacks((prev) => [...prev, row]); }, [dbInbounds]); const removeFallback = useCallback((idx: number) => { setFallbacks((prev) => prev.filter((_, i) => i !== idx)); }, []); const moveFallback = useCallback((idx: number, dir: number) => { setFallbacks((prev) => { const arr = [...prev]; const j = idx + dir; if (j < 0 || j >= arr.length) return prev; [arr[idx], arr[j]] = [arr[j], arr[idx]]; return arr; }); }, []); const onFallbackChildPicked = useCallback((rowKey: string, childId: number) => { setFallbacks((prev) => prev.map((row) => { if (row.rowKey !== rowKey) return row; const child = (dbInbounds || []).find((ib) => ib.id === childId); const defaults = deriveFallbackDefaults(child); return { ...row, childId, ...defaults }; })); }, [dbInbounds]); const updateFallback = useCallback((rowKey: string, patch: Partial) => { setFallbacks((prev) => prev.map((row) => (row.rowKey === rowKey ? { ...row, ...patch } : row))); }, []); const rederiveFallback = useCallback((rowKey: string) => { setFallbacks((prev) => prev.map((row) => { if (row.rowKey !== rowKey || !row.childId) return row; const child = (dbInbounds || []).find((ib) => ib.id === row.childId); const defaults = deriveFallbackDefaults(child); return { ...row, ...defaults }; })); messageApi.success(t('pages.inbounds.fallbacks.rederived') || 'Re-filled from child'); }, [dbInbounds, t, messageApi]); const quickAddAllFallbacks = useCallback(() => { const masterId = dbInbound?.id; const list = dbInbounds || []; setFallbacks((prev) => { const existing = new Set(prev.map((r) => r.childId).filter(Boolean)); const next = [...prev]; let added = 0; for (const ib of list) { if (ib.id === masterId) continue; if (existing.has(ib.id)) continue; let stream: StreamLike | undefined; try { stream = ib.toInbound()?.stream as StreamLike | undefined; } catch { continue; } if (!stream || !FALLBACK_ELIGIBLE_TRANSPORTS.has(stream.network ?? '')) continue; const row: FallbackRow = { rowKey: `fb-${++fallbackKeyRef.current}`, childId: ib.id, ...deriveFallbackDefaults(ib), }; next.push(row); added += 1; } if (added > 0) { messageApi.success(t('pages.inbounds.fallbacks.quickAdded', { n: added }) || `Added ${added} fallback(s)`); } else { messageApi.info(t('pages.inbounds.fallbacks.quickAddedNone') || 'No new eligible inbounds to add'); } return next; }); }, [dbInbound, dbInbounds, t, messageApi]); const fallbackChildOptions = useMemo(() => { const list = dbInbounds || []; const masterId = dbInbound?.id; return list .filter((ib) => ib.id !== masterId) .map((ib) => ({ label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`, value: ib.id, })); }, [dbInbounds, dbInbound]); const toggleFallbackEdit = useCallback((rowKey: string) => { setFallbackEditing((prev) => { const next = new Set(prev); if (next.has(rowKey)) next.delete(rowKey); else next.add(rowKey); return next; }); }, []); const describeFallback = useCallback((record: FallbackRow) => { const parts: string[] = []; if (record.name) parts.push(`SNI=${record.name}`); if (record.alpn) parts.push(`ALPN=${record.alpn}`); if (record.path) parts.push(`path=${record.path}`); const condition = parts.length ? `${t('pages.inbounds.fallbacks.routesWhen') || 'Routes when'} ${parts.join(' · ')}` : (t('pages.inbounds.fallbacks.defaultCatchAll') || 'Default — catches anything else'); const proxyTag = record.xver === 2 ? ' · PROXY v2' : record.xver === 1 ? ' · PROXY v1' : ''; return { condition, proxyTag }; }, [t]); const withSaving = useCallback(async (fn: () => Promise): Promise => { setSaving(true); try { return await fn(); } finally { setSaving(false); } }, []); const randomSSPassword = useCallback((target: ShadowsocksClient) => { if (target) { target.password = RandomUtil.randomShadowsocksPassword(inboundRef.current.settings.method); refresh(); } }, [refresh]); const regenWgKeypair = useCallback((target: WireguardPeer) => { const kp = Wireguard.generateKeypair(); target.publicKey = kp.publicKey; target.privateKey = kp.privateKey; refresh(); }, [refresh]); const regenInboundWg = useCallback(() => { const kp = Wireguard.generateKeypair(); inboundRef.current.settings.pubKey = kp.publicKey; inboundRef.current.settings.secretKey = kp.privateKey; refresh(); }, [refresh]); const genRealityKeypair = useCallback(async () => { await withSaving(async () => { const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert'); if (msg?.success) { const obj = msg.obj as { privateKey: string; publicKey: string }; inboundRef.current.stream.reality.privateKey = obj.privateKey; inboundRef.current.stream.reality.settings.publicKey = obj.publicKey; refresh(); } }); }, [withSaving, refresh]); const clearRealityKeypair = useCallback(() => { if (!inboundRef.current?.stream?.reality) return; inboundRef.current.stream.reality.privateKey = ''; inboundRef.current.stream.reality.settings.publicKey = ''; refresh(); }, [refresh]); const genMldsa65 = useCallback(async () => { await withSaving(async () => { const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65'); if (msg?.success) { const obj = msg.obj as { seed: string; verify: string }; inboundRef.current.stream.reality.mldsa65Seed = obj.seed; inboundRef.current.stream.reality.settings.mldsa65Verify = obj.verify; refresh(); } }); }, [withSaving, refresh]); const clearMldsa65 = useCallback(() => { if (!inboundRef.current?.stream?.reality) return; inboundRef.current.stream.reality.mldsa65Seed = ''; inboundRef.current.stream.reality.settings.mldsa65Verify = ''; refresh(); }, [refresh]); const randomizeRealityTarget = useCallback(() => { if (!inboundRef.current?.stream?.reality) return; const target = getRandomRealityTarget() as { target: string; sni: string }; inboundRef.current.stream.reality.target = target.target; inboundRef.current.stream.reality.serverNames = target.sni; refresh(); }, [refresh]); const randomizeShortIds = useCallback(() => { if (!inboundRef.current?.stream?.reality) return; inboundRef.current.stream.reality.shortIds = RandomUtil.randomShortIds(); refresh(); }, [refresh]); const getNewEchCert = useCallback(async () => { if (!inboundRef.current?.stream?.tls) return; await withSaving(async () => { const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inboundRef.current.stream.tls.sni, }); if (msg?.success) { const obj = msg.obj as { echServerKeys: string; echConfigList: string }; inboundRef.current.stream.tls.echServerKeys = obj.echServerKeys; inboundRef.current.stream.tls.settings.echConfigList = obj.echConfigList; refresh(); } }); }, [withSaving, refresh]); const clearEchCert = useCallback(() => { if (!inboundRef.current?.stream?.tls) return; inboundRef.current.stream.tls.echServerKeys = ''; inboundRef.current.stream.tls.settings.echConfigList = ''; refresh(); }, [refresh]); const setDefaultCertData = useCallback((idx: number) => { if (!inboundRef.current?.stream?.tls?.certs?.[idx]) return; inboundRef.current.stream.tls.certs[idx].certFile = defaultCert; inboundRef.current.stream.tls.certs[idx].keyFile = defaultKey; refresh(); }, [defaultCert, defaultKey, refresh]); const matchesVlessAuth = useCallback((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 = useCallback(async (authId: string) => { if (!authId || !inboundRef.current?.settings) return; await withSaving(async () => { 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; inboundRef.current.settings.decryption = block.decryption; inboundRef.current.settings.encryption = block.encryption; refresh(); }); }, [withSaving, refresh, matchesVlessAuth]); const clearVlessEnc = useCallback(() => { if (!inboundRef.current?.settings) return; inboundRef.current.settings.decryption = 'none'; inboundRef.current.settings.encryption = 'none'; refresh(); }, [refresh]); const selectedVlessAuth = useMemo(() => { const encryption = inboundRef.current?.settings?.encryption; if (!encryption || encryption === 'none') return 'None'; const parts = encryption.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'); // eslint-disable-next-line react-hooks/exhaustive-deps }, [inboundRef.current?.settings?.encryption, t]); const onSSMethodChange = useCallback(() => { const ib = inboundRef.current; ib.settings.password = RandomUtil.randomShadowsocksPassword(ib.settings.method); if (ib.isSSMultiUser) { ib.settings.shadowsockses.forEach((c: ShadowsocksClient) => { c.method = ib.isSS2022 ? '' : ib.settings.method; c.password = RandomUtil.randomShadowsocksPassword(ib.settings.method); }); } else { ib.settings.shadowsockses = []; } refresh(); }, [refresh]); const parseAdvancedSliceOrFallback = (rawText: string, fallback: unknown) => { if (!rawText?.trim()) return fallback; return JSON.parse(rawText); }; const settingsFallback = () => inboundRef.current?.settings?.toJson?.() || {}; const sniffingFallback = () => inboundRef.current?.sniffing?.toJson?.() || {}; const streamFallback = () => inboundRef.current?.stream?.toJson?.() || {}; const parseAdvancedSliceWithLabel = useCallback((rawText: string, fallback: unknown, label: string) => { try { return parseAdvancedSliceOrFallback(rawText, fallback); } catch (e) { messageApi.error(`${label} JSON invalid: ${(e as Error).message}`); throw e; } }, [messageApi]); const compactAdvancedJson = useCallback((raw: string, fallback: string, label: string) => { try { return JSON.stringify(JSON.parse(raw || fallback)); } catch (e) { messageApi.error(`${label} JSON invalid: ${(e as Error).message}`); throw e; } }, [messageApi]); const applyAdvancedJsonToBasic = useCallback((): boolean => { const ib = inboundRef.current; if (!ib) return true; let settings: unknown; let streamSettings: unknown; let sniffing: unknown; try { settings = parseAdvancedSliceWithLabel(advancedTextRef.current.settings, settingsFallback(), t('pages.inbounds.advanced.settings')); streamSettings = parseAdvancedSliceWithLabel(advancedTextRef.current.stream, streamFallback(), t('pages.inbounds.advanced.stream')); sniffing = parseAdvancedSliceWithLabel(advancedTextRef.current.sniffing, sniffingFallback(), t('pages.inbounds.advanced.sniffing')); } catch { return false; } try { inboundRef.current = Inbound.fromJson({ port: ib.port, listen: ib.listen, protocol: ib.protocol, settings, streamSettings, tag: ib.tag, sniffing, clientStats: ib.clientStats, }); refresh(); } catch (e) { messageApi.error(`${t('pages.inbounds.advanced.jsonErrorPrefix')}: ${(e as Error).message}`); return false; } return true; }, [t, refresh, parseAdvancedSliceWithLabel, messageApi]); const handleTabChange = (next: string) => { if (document.activeElement instanceof HTMLElement) { document.activeElement.blur(); } if (activeTabKey === 'advanced' && next !== 'advanced') { if (!applyAdvancedJsonToBasic()) return; } setActiveTabKey(next); }; const unwrapWrappedObject = (parsed: unknown, key: string): unknown => { if ( parsed && typeof parsed === 'object' && !Array.isArray(parsed) && (parsed as Record)[key] !== undefined ) { return (parsed as Record)[key]; } return parsed; }; const wrappedConfigValue = (key: string, slice: 'stream' | 'sniffing' | 'settings'): string => { const ib = inboundRef.current; if (!ib) return ''; try { const fb = slice === 'settings' ? settingsFallback() : slice === 'sniffing' ? sniffingFallback() : streamFallback(); const value = parseAdvancedSliceOrFallback(advancedTextRef.current[slice], fb); return JSON.stringify({ [key]: value }, null, 2); } catch { return ''; } }; const setWrappedConfigValue = (key: string, slice: 'stream' | 'sniffing' | 'settings', label: string, next: string) => { let parsed: unknown; try { parsed = JSON.parse(next); } catch (e) { messageApi.error(`${label} JSON invalid: ${(e as Error).message}`); return; } const unwrapped = unwrapWrappedObject(parsed, key); if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) { messageApi.error(`${label} JSON must be an object or { ${key}: { ... } }.`); return; } try { advancedTextRef.current[slice] = JSON.stringify(unwrapped, null, 2); refresh(); } catch (e) { messageApi.error(`${label} JSON invalid: ${(e as Error).message}`); } }; const advancedAllValue = (() => { const ib = inboundRef.current; if (!ib) return ''; try { const result: Record = { listen: ib.listen, port: ib.port, protocol: ib.protocol, settings: parseAdvancedSliceOrFallback(advancedTextRef.current.settings, settingsFallback()), sniffing: parseAdvancedSliceOrFallback(advancedTextRef.current.sniffing, sniffingFallback()), tag: ib.tag, }; if (canEnableStream) { result.streamSettings = parseAdvancedSliceOrFallback(advancedTextRef.current.stream, streamFallback()); } return JSON.stringify(result, null, 2); } catch { return ''; } })(); const setAdvancedAllValue = (next: string) => { let parsedRaw: unknown; try { parsedRaw = JSON.parse(next); } catch (e) { messageApi.error(`All JSON invalid: ${(e as Error).message}`); return; } if (!parsedRaw || typeof parsedRaw !== 'object' || Array.isArray(parsedRaw)) { messageApi.error('All JSON must be an inbound object.'); return; } const parsed = parsedRaw as { listen?: string; port?: number | string; protocol?: string; tag?: string; settings?: unknown; sniffing?: unknown; streamSettings?: unknown; }; const ib = inboundRef.current; try { if (typeof parsed.listen === 'string') ib.listen = parsed.listen; if (parsed.port !== undefined) { const port = Number(parsed.port); if (Number.isFinite(port)) ib.port = port; } if (typeof parsed.protocol === 'string' && PROTOCOLS.includes(parsed.protocol)) { ib.protocol = parsed.protocol; } if (typeof parsed.tag === 'string') ib.tag = parsed.tag; const existingSettings = parseAdvancedSliceOrFallback(advancedTextRef.current.settings, settingsFallback()); advancedTextRef.current.settings = JSON.stringify(parsed.settings ?? existingSettings, null, 2); advancedTextRef.current.sniffing = JSON.stringify(parsed.sniffing ?? sniffingFallback(), null, 2); advancedTextRef.current.stream = canEnableStream ? JSON.stringify(parsed.streamSettings ?? streamFallback(), null, 2) : '{}'; refresh(); } catch (e) { messageApi.error(`All JSON invalid: ${(e as Error).message}`); } }; const saveFallbacks = useCallback(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; }, [fallbacks]); const submit = useCallback(async () => { const ib = inboundRef.current; const form = dbFormRef.current; if (!ib || !form) return; setSaving(true); try { let streamSettings: string; let sniffing: string; let settings: string; try { streamSettings = canEnableStream ? compactAdvancedJson(advancedTextRef.current.stream, '', t('pages.inbounds.advanced.stream')) : (ib.stream?.sockopt ? JSON.stringify({ sockopt: ib.stream.sockopt.toJson() }) : ''); sniffing = compactAdvancedJson(advancedTextRef.current.sniffing, ib.sniffing.toString(), t('pages.inbounds.advanced.sniffing')); settings = compactAdvancedJson(advancedTextRef.current.settings, ib.settings.toString(), t('pages.inbounds.advanced.settings')); } catch { return; } const baseCheck = InboundFormSchema.safeParse({ remark: form.remark ?? '', enable: !!form.enable, port: Number(ib.port), listen: ib.listen ?? '', protocol: ib.protocol ?? '', }); if (!baseCheck.success) { const issue = baseCheck.error.issues[0]; messageApi.error(t(issue?.message ?? 'somethingWentWrong', { defaultValue: issue?.message ?? 'invalid' })); return; } const payload: Record = { up: form.up || 0, down: form.down || 0, total: form.total, remark: form.remark, enable: form.enable, expiryTime: form.expiryTime, trafficReset: form.trafficReset, lastTrafficResetTime: form.lastTrafficResetTime || 0, listen: ib.listen, port: ib.port, protocol: ib.protocol, settings, streamSettings, sniffing, }; if (form.nodeId != null) payload.nodeId = form.nodeId; const url = mode === 'edit' ? `/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); if (masterId) await saveFallbacks(masterId); } onSaved(); onClose(); } } finally { setSaving(false); } }, [canEnableStream, compactAdvancedJson, t, messageApi, mode, dbInbound, isFallbackHost, saveFallbacks, onSaved, onClose]); const protocolSnapshot = inboundRef.current?.protocol; const streamSnapshot = JSON.stringify(inboundRef.current?.stream?.toJson?.() || {}); const sniffingSnapshot = JSON.stringify(inboundRef.current?.sniffing?.toJson?.() || {}); const settingsSnapshot = JSON.stringify(inboundRef.current?.settings?.toJson?.() || {}); useEffect(() => { if (!inboundRef.current) return; (['stream', 'sniffing', 'settings'] as const).forEach(stampAdvancedTextFor); }, [protocolSnapshot, streamSnapshot, sniffingSnapshot, settingsSnapshot, stampAdvancedTextFor]); const title = mode === 'edit' ? t('pages.inbounds.modifyInbound') : t('pages.inbounds.addInbound'); const okText = mode === 'edit' ? t('pages.clients.submitEdit') : t('create'); const ib = inboundRef.current; const form = dbFormRef.current; if (!ib || !form) { return ; } const totalGB = form.total ? Math.round((form.total / SizeFormatter.ONE_GB) * 100) / 100 : 0; const expiryDate: Dayjs | null = form.expiryTime > 0 ? dayjs(form.expiryTime) : null; const renderBasicsTab = () => (
{ form.enable = v; refresh(); }} /> { form.remark = e.target.value; refresh(); }} /> {selectableNodes.length > 0 && isNodeEligible && ( )} { ib.listen = e.target.value; refresh(); }} /> { ib.port = Number(v) || 0; refresh(); }} /> {t('pages.inbounds.totalFlow')}}> { form.total = NumberFormatter.toFixed((Number(v) || 0) * SizeFormatter.ONE_GB, 0); refresh(); }} /> {t('pages.inbounds.expireDate')}}> { form.expiryTime = d ? d.valueOf() : 0; refresh(); }} />
); const renderFallbacksCard = () => ( {t('pages.inbounds.fallbacks.help') || 'When a connection on this inbound does not match any client, route it to another inbound. Pick a child below and the routing fields (SNI / ALPN / path / xver) auto-fill from its transport — most setups need no further tweaking. Each child should listen on 127.0.0.1 with security=none.'} {fallbacks.length === 0 && ( )} {fallbacks.map((record, index) => (
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 renderProtocolTab = () => ( <> {isVlessLike && (
{ ib.settings.decryption = e.target.value; refresh(); }} /> { ib.settings.encryption = e.target.value; refresh(); }} /> {t('pages.inbounds.vlessAuthSelected', { auth: selectedVlessAuth })}
)} {isFallbackHost && renderFallbacksCard()} {ib.protocol === Protocols.SHADOWSOCKS && (
{ib.isSS2022 && ( Password randomSSPassword(ib.settings)} />}> { ib.settings.password = e.target.value; refresh(); }} /> )} { ib.settings.ivCheck = v; refresh(); }} />
)} {(ib.protocol === Protocols.HTTP || ib.protocol === Protocols.MIXED) && (
{(ib.settings.accounts || []).map((account: HttpAccount, idx: number) => ( {String(idx + 1)} { account.user = e.target.value; refresh(); }} /> { account.pass = e.target.value; refresh(); }} /> ))} {ib.protocol === Protocols.HTTP && ( { ib.settings.allowTransparent = v; refresh(); }} /> )} {ib.protocol === Protocols.MIXED && ( <> { ib.settings.udp = v; refresh(); }} /> {ib.settings.udp && ( { ib.settings.ip = e.target.value; refresh(); }} /> )} )}
)} {ib.protocol === Protocols.TUNNEL && (
{ ib.settings.rewriteAddress = e.target.value; refresh(); }} /> { ib.settings.rewritePort = Number(v) || 0; refresh(); }} /> {(ib.settings.portMap || []).length > 0 && ( {(ib.settings.portMap as { name: string; value: string }[]).map((pm, idx) => ( {String(idx + 1)} { pm.name = e.target.value; refresh(); }} /> { pm.value = e.target.value; refresh(); }} /> ))} )} { ib.settings.followRedirect = v; refresh(); }} />
)} {ib.protocol === Protocols.TUN && (
{ ib.settings.name = e.target.value; refresh(); }} /> { ib.settings.mtu = Number(v) || 0; refresh(); }} /> {(ib.settings.gateway || []).map((_ip: string, j: number) => ( { ib.settings.gateway[j] = e.target.value; refresh(); }} /> ))} {(ib.settings.dns || []).map((_ip: string, j: number) => ( { ib.settings.dns[j] = e.target.value; refresh(); }} /> ))} { ib.settings.userLevel = Number(v) || 0; refresh(); }} /> Auto system routes}> {(ib.settings.autoSystemRoutingTable || []).map((_ip: string, j: number) => ( { ib.settings.autoSystemRoutingTable[j] = e.target.value; refresh(); }} /> ))} Auto outbounds interface}> { ib.settings.autoOutboundsInterface = e.target.value; refresh(); }} />
)} {ib.protocol === Protocols.WIREGUARD && (
Secret key }> { ib.settings.secretKey = e.target.value; refresh(); }} /> { ib.settings.mtu = Number(v) || 0; refresh(); }} /> { ib.settings.noKernelTun = v; refresh(); }} /> {(ib.settings.peers || []).map((peer: WireguardPeer, idx: number) => (
Peer {idx + 1} {ib.settings.peers.length > 1 && ( { ib.settings.delPeer(idx); refresh(); }} /> )} Secret key regenWgKeypair(peer)} />}> { peer.privateKey = e.target.value; refresh(); }} /> { peer.publicKey = e.target.value; refresh(); }} /> { peer.psk = e.target.value; refresh(); }} /> {(peer.allowedIPs || []).map((_ip: string, j: number) => ( { peer.allowedIPs[j] = e.target.value; refresh(); }} /> {peer.allowedIPs.length > 1 && ( )} ))} { peer.keepAlive = Number(v) || 0; refresh(); }} />
))}
)} ); const renderStreamTab = () => { const network = ib.stream?.network; return ( <>
{ib.protocol !== Protocols.HYSTERIA && ( )} {network === 'tcp' && ( <> {canEnableTls && ( { ib.stream.tcp.acceptProxyProtocol = v; refresh(); }} /> )} { ib.stream.tcp.type = v ? 'http' : 'none'; refresh(); }} /> {ib.stream.tcp.type === 'http' && ( <> {t('pages.inbounds.stream.general.request')} { ib.stream.tcp.request.version = e.target.value; refresh(); }} /> { ib.stream.tcp.request.method = e.target.value; refresh(); }} /> {t('pages.inbounds.stream.tcp.path')} }> {(ib.stream.tcp.request.path || []).map((_p: string, idx: number) => ( { ib.stream.tcp.request.path[idx] = e.target.value; refresh(); }} /> {ib.stream.tcp.request.path.length > 1 && ( )} ))} {(ib.stream.tcp.request.headers || []).length > 0 && ( {(ib.stream.tcp.request.headers as { name: string; value: string }[]).map((h, idx) => ( {String(idx + 1)} { h.name = e.target.value; refresh(); }} /> { h.value = e.target.value; refresh(); }} /> ))} )} {t('pages.inbounds.stream.general.response')} { ib.stream.tcp.response.version = e.target.value; refresh(); }} /> { ib.stream.tcp.response.status = e.target.value; refresh(); }} /> { ib.stream.tcp.response.reason = e.target.value; refresh(); }} /> {(ib.stream.tcp.response.headers || []).length > 0 && ( {(ib.stream.tcp.response.headers as { name: string; value: string }[]).map((h, idx) => ( {String(idx + 1)} { h.name = e.target.value; refresh(); }} /> { h.value = e.target.value; refresh(); }} /> ))} )} )} )} {network === 'kcp' && ( <> { ib.stream.kcp.mtu = Number(v) || 0; refresh(); }} /> { ib.stream.kcp.tti = Number(v) || 0; refresh(); }} /> { ib.stream.kcp.upCap = Number(v) || 0; refresh(); }} /> { ib.stream.kcp.downCap = Number(v) || 0; refresh(); }} /> { ib.stream.kcp.cwndMultiplier = Number(v) || 0; refresh(); }} /> { ib.stream.kcp.maxSendingWindow = Number(v) || 0; refresh(); }} /> )} {network === 'ws' && ( <> { ib.stream.ws.acceptProxyProtocol = v; refresh(); }} /> { ib.stream.ws.host = e.target.value; refresh(); }} /> { ib.stream.ws.path = e.target.value; refresh(); }} /> { ib.stream.ws.heartbeatPeriod = Number(v) || 0; refresh(); }} /> {(ib.stream.ws.headers || []).length > 0 && ( {(ib.stream.ws.headers as { name: string; value: string }[]).map((h, idx) => ( {String(idx + 1)} { h.name = e.target.value; refresh(); }} /> { h.value = e.target.value; refresh(); }} /> ))} )} )} {network === 'grpc' && ( <> { ib.stream.grpc.serviceName = e.target.value; refresh(); }} /> { ib.stream.grpc.authority = e.target.value; refresh(); }} /> { ib.stream.grpc.multiMode = v; refresh(); }} /> )} {network === 'httpupgrade' && ( <> { ib.stream.httpupgrade.acceptProxyProtocol = v; refresh(); }} /> { ib.stream.httpupgrade.host = e.target.value; refresh(); }} /> { ib.stream.httpupgrade.path = e.target.value; refresh(); }} /> {(ib.stream.httpupgrade.headers || []).length > 0 && ( {(ib.stream.httpupgrade.headers as { name: string; value: string }[]).map((h, idx) => ( {String(idx + 1)} { h.name = e.target.value; refresh(); }} /> { h.value = e.target.value; refresh(); }} /> ))} )} )} {network === 'xhttp' && ( <> { ib.stream.xhttp.host = e.target.value; refresh(); }} /> { ib.stream.xhttp.path = e.target.value; refresh(); }} /> {(ib.stream.xhttp.headers || []).length > 0 && ( {(ib.stream.xhttp.headers as { name: string; value: string }[]).map((h, idx) => ( {String(idx + 1)} { h.name = e.target.value; refresh(); }} /> { h.value = e.target.value; refresh(); }} /> ))} )} {ib.stream.xhttp.mode === 'packet-up' && ( <> { ib.stream.xhttp.scMaxBufferedPosts = Number(v) || 0; refresh(); }} /> { ib.stream.xhttp.scMaxEachPostBytes = e.target.value; refresh(); }} /> )} {ib.stream.xhttp.mode === 'stream-up' && ( { ib.stream.xhttp.scStreamUpServerSecs = e.target.value; refresh(); }} /> )} { ib.stream.xhttp.serverMaxHeaderBytes = Number(v) || 0; refresh(); }} /> { ib.stream.xhttp.xPaddingBytes = e.target.value; refresh(); }} /> { ib.stream.xhttp.xPaddingObfsMode = v; refresh(); }} /> {ib.stream.xhttp.xPaddingObfsMode && ( <> { ib.stream.xhttp.xPaddingKey = e.target.value; refresh(); }} /> { ib.stream.xhttp.xPaddingHeader = e.target.value; refresh(); }} /> )} {ib.stream.xhttp.sessionPlacement && ib.stream.xhttp.sessionPlacement !== 'path' && ( { ib.stream.xhttp.sessionKey = e.target.value; refresh(); }} /> )} {ib.stream.xhttp.seqPlacement && ib.stream.xhttp.seqPlacement !== 'path' && ( { ib.stream.xhttp.seqKey = e.target.value; refresh(); }} /> )} {ib.stream.xhttp.mode === 'packet-up' && ( )} {ib.stream.xhttp.mode === 'packet-up' && ib.stream.xhttp.uplinkDataPlacement && ib.stream.xhttp.uplinkDataPlacement !== 'body' && ( { ib.stream.xhttp.uplinkDataKey = e.target.value; refresh(); }} /> )} { ib.stream.xhttp.noSSEHeader = v; refresh(); }} /> )} {externalProxyOn && ( )} {externalProxyOn && ( {(ib.stream.externalProxy as { forceTls: string; dest: string; port: number; remark: string; sni?: string; fingerprint?: string; alpn?: string[] }[]).map((row, idx) => (
{ row.dest = e.target.value; refresh(); }} /> { row.port = Number(v) || 0; refresh(); }} /> { row.remark = e.target.value; refresh(); }} /> { ib.stream.externalProxy.splice(idx, 1); refresh(); }}> {row.forceTls === 'tls' && ( { row.sni = e.target.value; refresh(); }} /> )}
))}
)} { ib.stream.sockoptSwitch = v; refresh(); }} /> {ib.stream.sockoptSwitch && ib.stream.sockopt && ( <> { ib.stream.sockopt.mark = Number(v) || 0; refresh(); }} /> { ib.stream.sockopt.tcpKeepAliveInterval = Number(v) || 0; refresh(); }} /> { ib.stream.sockopt.tcpKeepAliveIdle = Number(v) || 0; refresh(); }} /> { ib.stream.sockopt.tcpMaxSeg = Number(v) || 0; refresh(); }} /> { ib.stream.sockopt.tcpUserTimeout = Number(v) || 0; refresh(); }} /> { ib.stream.sockopt.tcpWindowClamp = Number(v) || 0; refresh(); }} /> { ib.stream.sockopt.acceptProxyProtocol = v; refresh(); }} /> { ib.stream.sockopt.tcpFastOpen = v; refresh(); }} /> { ib.stream.sockopt.tcpMptcp = v; refresh(); }} /> { ib.stream.sockopt.penetrate = v; refresh(); }} /> { ib.stream.sockopt.V6Only = v; refresh(); }} /> { ib.stream.sockopt.dialerProxy = e.target.value; refresh(); }} /> { ib.stream.sockopt.interfaceName = e.target.value; refresh(); }} /> )} {ib.protocol === Protocols.HYSTERIA && ( <> Version}> { ib.stream.hysteria.version = Number(v) || 2; refresh(); }} /> UDP idle timeout}> { ib.stream.hysteria.udpIdleTimeout = Number(v) || 0; refresh(); }} /> { ib.stream.hysteria.masqueradeSwitch = v; refresh(); }} /> {ib.stream.hysteria.masqueradeSwitch && ( <> {ib.stream.hysteria.masquerade.type === 'proxy' && ( <> { ib.stream.hysteria.masquerade.url = e.target.value; refresh(); }} /> { ib.stream.hysteria.masquerade.rewriteHost = v; refresh(); }} /> { ib.stream.hysteria.masquerade.insecure = v; refresh(); }} /> )} {ib.stream.hysteria.masquerade.type === 'file' && ( { ib.stream.hysteria.masquerade.dir = e.target.value; refresh(); }} /> )} {ib.stream.hysteria.masquerade.type === 'string' && ( <>