InboundFormModal.tsx 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254
  1. import { useEffect, useRef, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { QuestionCircleOutlined } from '@ant-design/icons';
  4. import dayjs from 'dayjs';
  5. import {
  6. Alert,
  7. Form,
  8. Input,
  9. InputNumber,
  10. Modal,
  11. Radio,
  12. Select,
  13. Switch,
  14. Tabs,
  15. Tooltip,
  16. message,
  17. } from 'antd';
  18. import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form';
  19. import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils';
  20. import type { RealityScanResult } from '@/generated/types';
  21. import {
  22. rawInboundToFormValues,
  23. formValuesToWirePayload,
  24. withoutClients,
  25. } from '@/lib/xray/inbound-form-adapter';
  26. import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults';
  27. import { generateAwgObfuscation } from '@/lib/xray/amneziawg-obfuscation';
  28. import { composeInboundTag, isAutoInboundTag, type InboundTagInput } from '@/lib/xray/inbound-tag';
  29. import {
  30. canEnableReality,
  31. canEnableSniffing,
  32. canEnableStream,
  33. canEnableTls,
  34. isSS2022,
  35. } from '@/lib/xray/protocol-capabilities';
  36. import {
  37. InboundDbFieldsSchema,
  38. InboundFormBaseSchema,
  39. InboundFormSchema,
  40. type InboundFormValues,
  41. } from '@/schemas/forms/inbound-form';
  42. import { FormField, rhfZodValidate } from '@/components/form/rhf';
  43. import { Protocols, TRAFFIC_RESETS } from '@/schemas/primitives';
  44. import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
  45. import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria';
  46. import {
  47. createHysteriaTlsSettingsWithDefaultCert,
  48. createMasqueTlsSettingsWithDefaultCert,
  49. } from '@/lib/xray/inbound-tls-defaults';
  50. import { MASQUE_DEFAULT_PATH } from '@/schemas/protocols/stream/masque';
  51. import { NODE_ELIGIBLE_PROTOCOLS } from '@/lib/xray/node-protocols';
  52. import { VLESS_AUTH_LABEL_KEYS, vlessEncryptionAuthKind } from '@/lib/xray/vless-encryption';
  53. import { SniffingSchema } from '@/schemas/primitives/sniffing';
  54. import { TcpStreamSettingsSchema } from '@/schemas/protocols/stream/tcp';
  55. import { KcpStreamSettingsSchema } from '@/schemas/protocols/stream/kcp';
  56. import { WsStreamSettingsSchema } from '@/schemas/protocols/stream/ws';
  57. import { GrpcStreamSettingsSchema } from '@/schemas/protocols/stream/grpc';
  58. import { HttpUpgradeStreamSettingsSchema } from '@/schemas/protocols/stream/httpupgrade';
  59. import { XHttpStreamSettingsSchema } from '@/schemas/protocols/stream/xhttp';
  60. import { XDriveStreamSettingsSchema } from '@/schemas/protocols/stream/xdrive';
  61. import { DateTimePicker } from '@/components/form';
  62. import { FinalMaskField } from '@/lib/xray/forms/fields';
  63. import XDriveForm from '@/lib/xray/forms/transport/XDriveForm';
  64. import './InboundFormModal.css';
  65. import { AdvancedAllEditor, AdvancedSliceEditor } from './advanced-editors';
  66. import { formatInboundIssue, formatInboundValidation } from './formatValidationError';
  67. import {
  68. AmneziawgFields,
  69. HttpFields,
  70. HysteriaFields,
  71. MasqueSettingsFields,
  72. MasqueStreamFields,
  73. MixedFields,
  74. MtprotoFields,
  75. ShadowsocksFields,
  76. TuicFields,
  77. TunFields,
  78. TunnelFields,
  79. VlessFields,
  80. WireguardFields,
  81. } from './protocols';
  82. import {
  83. GrpcForm,
  84. HttpUpgradeForm,
  85. KcpForm,
  86. RawForm,
  87. SockoptForm,
  88. WsForm,
  89. XhttpForm,
  90. } from './transport';
  91. import { RealityForm, TlsForm } from './security';
  92. import { useSecurityActions } from './useSecurityActions';
  93. import { useInboundFallbacks } from './useInboundFallbacks';
  94. import FallbacksCard from './FallbacksCard';
  95. import SniffingTab from './SniffingTab';
  96. import type { DBInbound } from '@/models/dbinbound';
  97. import type { NodeRecord } from '@/api/queries/useNodesQuery';
  98. /* Render a field label with a hover tooltip icon instead of an `extra` help line below. */
  99. const labelWithHint = (label: string, hint: string) => (
  100. <span>
  101. {label}
  102. <Tooltip title={hint}>
  103. <QuestionCircleOutlined style={{ marginInlineStart: 4, color: 'rgba(128,128,128,0.65)' }} />
  104. </Tooltip>
  105. </span>
  106. );
  107. const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
  108. const SHARE_ADDR_STRATEGIES = ['node', 'listen', 'custom'] as const;
  109. const SHARE_ADDR_HOSTNAME_RE =
  110. /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$/;
  111. function isValidShareAddrInput(value: string): boolean {
  112. const v = value.trim();
  113. if (v.length === 0) return true;
  114. if (v.includes('://') || v.startsWith('//') || /[/?#@]/.test(v)) return false;
  115. if (v.startsWith('[')) {
  116. if (!v.endsWith(']')) return false;
  117. try {
  118. new URL(`http://${v}`);
  119. return true;
  120. } catch {
  121. return false;
  122. }
  123. }
  124. if (v.includes(':')) {
  125. try {
  126. new URL(`http://[${v}]`);
  127. return true;
  128. } catch {
  129. return false;
  130. }
  131. }
  132. return SHARE_ADDR_HOSTNAME_RE.test(v);
  133. }
  134. interface RhfValidationIssue {
  135. path: PropertyKey[];
  136. message: string;
  137. }
  138. function firstRhfValidationIssue(
  139. value: unknown,
  140. path: PropertyKey[] = [],
  141. ): RhfValidationIssue | null {
  142. if (!value || typeof value !== 'object') return null;
  143. const record = value as Record<string, unknown>;
  144. // `type` is what marks a react-hook-form leaf FieldError; anything else is a group.
  145. if ('type' in record) {
  146. return { path, message: typeof record.message === 'string' ? record.message : '' };
  147. }
  148. for (const key of Object.keys(record)) {
  149. const issue = firstRhfValidationIssue(record[key], [...path, key]);
  150. if (issue) return issue;
  151. }
  152. return null;
  153. }
  154. function tabForValidationPath(path: PropertyKey[]): string {
  155. if (path[0] === 'settings') return 'protocol';
  156. if (path[0] === 'sniffing') return 'sniffing';
  157. if (path[0] === 'streamSettings') {
  158. if (path[1] === 'security' || path[1] === 'realitySettings' || path[1] === 'tlsSettings')
  159. return 'security';
  160. return 'stream';
  161. }
  162. return 'basic';
  163. }
  164. interface InboundFormModalProps {
  165. open: boolean;
  166. onClose: () => void;
  167. onSaved: () => void;
  168. mode: 'add' | 'edit';
  169. dbInbound: DBInbound | null;
  170. dbInbounds: DBInbound[];
  171. availableNodes?: NodeRecord[];
  172. availableNodesFetched?: boolean;
  173. }
  174. function buildAddModeValues(): InboundFormValues {
  175. const settings = createDefaultInboundSettings('vless') ?? undefined;
  176. return rawInboundToFormValues({
  177. protocol: 'vless',
  178. settings,
  179. streamSettings: {
  180. network: 'tcp',
  181. security: 'none',
  182. tcpSettings: TcpStreamSettingsSchema.parse({ header: { type: 'none' } }),
  183. },
  184. sniffing: SniffingSchema.parse({}),
  185. port: RandomUtil.randomInteger(10000, 60000),
  186. listen: '',
  187. tag: '',
  188. enable: true,
  189. trafficReset: 'never',
  190. });
  191. }
  192. /*
  193. * Switching `network` swaps which per-network key (tcpSettings, wsSettings,
  194. * grpcSettings, ...) appears on the wire. Seed each network's blob with its
  195. * Zod schema defaults so every field inside the network sub-form has a
  196. * defined starting value (KCP needs MTU=1350 etc., XHTTP needs the ""
  197. * sentinels so the "Default" option shows instead of blank).
  198. */
  199. function newStreamSlice(n: string): Record<string, unknown> {
  200. switch (n) {
  201. case 'tcp':
  202. return TcpStreamSettingsSchema.parse({ header: { type: 'none' } });
  203. case 'kcp':
  204. return KcpStreamSettingsSchema.parse({});
  205. case 'ws':
  206. return WsStreamSettingsSchema.parse({});
  207. case 'grpc':
  208. return GrpcStreamSettingsSchema.parse({});
  209. case 'httpupgrade':
  210. return HttpUpgradeStreamSettingsSchema.parse({});
  211. case 'xhttp':
  212. return XHttpStreamSettingsSchema.parse({});
  213. case 'xdrive':
  214. return XDriveStreamSettingsSchema.parse({});
  215. default:
  216. return {};
  217. }
  218. }
  219. export default function InboundFormModal({
  220. open,
  221. onClose,
  222. onSaved,
  223. mode,
  224. dbInbound,
  225. dbInbounds,
  226. availableNodes,
  227. availableNodesFetched = true,
  228. }: InboundFormModalProps) {
  229. const { t } = useTranslation();
  230. const [messageApi, messageContextHolder] = message.useMessage();
  231. const [modal, modalContextHolder] = Modal.useModal();
  232. const methods = useForm<InboundFormValues>({ defaultValues: buildAddModeValues() });
  233. const setV = methods.setValue as unknown as (name: string, value: unknown) => void;
  234. const getV = methods.getValues as unknown as (name?: string) => unknown;
  235. const control = methods.control;
  236. const [saving, setSaving] = useState(false);
  237. const [scanning, setScanning] = useState(false);
  238. const [scanResult, setScanResult] = useState<RealityScanResult | null>(null);
  239. const [activeTab, setActiveTab] = useState('basic');
  240. const {
  241. fallbacks,
  242. fallbackChildOptions,
  243. loadFallbacks,
  244. saveFallbacks,
  245. addFallback,
  246. updateFallback,
  247. removeFallback,
  248. moveFallback,
  249. addAllFallbacks,
  250. } = useInboundFallbacks(dbInbound, dbInbounds);
  251. const selectableNodes = (availableNodes || []).filter((n) => n.enable);
  252. const protocol = (useWatch({ control, name: 'protocol' }) ?? '') as string;
  253. const isNodeEligible = !!NODE_ELIGIBLE_PROTOCOLS[protocol];
  254. /*
  255. * The `node` share-address strategy only means something when the inbound can
  256. * actually live on a node — otherwise the node address it would resolve to is
  257. * always empty. Offer it only then; `listen`/`custom` work for local inbounds.
  258. */
  259. const nodeShareOptionAvailable = selectableNodes.length > 0 && isNodeEligible;
  260. const vlessEncryption = useWatch({ control, name: 'settings.encryption' }) ?? '';
  261. const ssMethod = useWatch({ control, name: 'settings.method' });
  262. const isSSWith2022 = isSS2022({
  263. protocol,
  264. settings: typeof ssMethod === 'string' ? { method: ssMethod } : {},
  265. });
  266. const mixedUdpOn = (useWatch({ control, name: 'settings.udp' }) ?? false) as boolean;
  267. const network = (useWatch({ control, name: 'streamSettings.network' }) ?? '') as string;
  268. const security = (useWatch({ control, name: 'streamSettings.security' }) ?? 'none') as string;
  269. const streamEnabled = canEnableStream({ protocol });
  270. const sniffingSupported = canEnableSniffing({ protocol });
  271. /*
  272. * Wireguard (always a UDP listener) and Tunnel (dokodemo-door) expose no
  273. * user-selectable transport — their stream tab is just sockopt, which is all
  274. * Tunnel's TProxy/redirect mode needs (sockopt.tproxy). Hysteria carries its
  275. * own dedicated transport form. For all of these the RAW/mKCP/WS/... network
  276. * picker and the per-network sub-forms are hidden.
  277. */
  278. const hasSelectableTransport =
  279. protocol !== Protocols.HYSTERIA &&
  280. protocol !== Protocols.MASQUE &&
  281. protocol !== Protocols.WIREGUARD &&
  282. protocol !== Protocols.TUNNEL &&
  283. protocol !== Protocols.TUIC;
  284. const wPort = useWatch({ control, name: 'port' });
  285. const wListen = (useWatch({ control, name: 'listen' }) ?? '') as string;
  286. const isUdsListen = wListen.startsWith('/') || wListen.startsWith('@');
  287. const wNodeId = useWatch({ control, name: 'nodeId' }) ?? null;
  288. const shareAddrStrategy = useWatch({ control, name: 'shareAddrStrategy' }) ?? 'node';
  289. const wTag = (useWatch({ control, name: 'tag' }) ?? '') as string;
  290. const wSsNetwork = useWatch({ control, name: 'settings.network' });
  291. const wTunnelNetwork = useWatch({ control, name: 'settings.allowedNetwork' });
  292. const wTotal = (useWatch({ control, name: 'total' }) as number | undefined) ?? 0;
  293. const wExpiry = (useWatch({ control, name: 'expiryTime' }) as number | undefined) ?? 0;
  294. const trafficReset = useWatch({ control, name: 'trafficReset' }) ?? 'never';
  295. const autoTagRef = useRef(true);
  296. const lastWrittenTagRef = useRef('');
  297. const currentTagInput = (): InboundTagInput => ({
  298. port: typeof wPort === 'number' ? wPort : 0,
  299. nodeId: typeof wNodeId === 'number' ? wNodeId : null,
  300. protocol,
  301. streamSettings: { network },
  302. settings: { network: wSsNetwork, allowedNetwork: wTunnelNetwork, udp: mixedUdpOn },
  303. });
  304. const isFallbackHost =
  305. (protocol === Protocols.VLESS || protocol === Protocols.TROJAN) &&
  306. network === 'tcp' &&
  307. (security === 'tls' || security === 'reality');
  308. const {
  309. genRealityKeypair,
  310. clearRealityKeypair,
  311. genMldsa65,
  312. clearMldsa65,
  313. scanRealityTarget,
  314. scanRealityCandidates,
  315. applyRealityScanResult,
  316. randomizeShortIds,
  317. randomizeSpiderX,
  318. getNewEchCert,
  319. clearEchCert,
  320. pinFromCert,
  321. pinFromRemote,
  322. setCertFromPanel,
  323. clearCertFiles,
  324. onSecurityChange,
  325. } = useSecurityActions({
  326. methods,
  327. setSaving,
  328. messageApi,
  329. modal,
  330. nodeId: typeof wNodeId === 'number' ? wNodeId : null,
  331. setScanResult,
  332. setScanning,
  333. });
  334. const toggleSockopt = (on: boolean) => {
  335. if (on) {
  336. setV('streamSettings.sockopt', SockoptStreamSettingsSchema.parse({}));
  337. } else {
  338. setV('streamSettings.sockopt', undefined);
  339. }
  340. };
  341. const wgSecretKey = useWatch({ control, name: 'settings.secretKey' });
  342. const wgPubKey =
  343. typeof wgSecretKey === 'string' && wgSecretKey.length > 0
  344. ? Wireguard.generateKeypair(wgSecretKey).publicKey
  345. : '';
  346. const regenInboundWg = () => {
  347. const kp = Wireguard.generateKeypair();
  348. setV('settings.secretKey', kp.privateKey);
  349. };
  350. // AmneziaWG uses the same Curve25519 keys as WireGuard, just nested under
  351. // settings.server instead of flat on settings — see amneziawg.ts. Unlike
  352. // WireGuard's Xray-native inbound (which re-derives its public key at
  353. // runtime and never stores one), AmneziaWG's server.publicKey is a real,
  354. // persisted field the Go backend reads directly, so it must be kept in
  355. // sync even when the user free-types a new private key instead of using
  356. // the regenerate button.
  357. const awgPrivateKey = useWatch({ control, name: 'settings.server.privateKey' });
  358. const awgPubKey =
  359. typeof awgPrivateKey === 'string' && awgPrivateKey.length > 0
  360. ? Wireguard.generateKeypair(awgPrivateKey).publicKey
  361. : '';
  362. useEffect(() => {
  363. if (protocol === Protocols.AMNEZIAWG) {
  364. setV('settings.server.publicKey', awgPubKey);
  365. }
  366. /* eslint-disable-next-line react-hooks/exhaustive-deps */
  367. }, [awgPubKey, protocol]);
  368. const regenInboundAwg = () => {
  369. const kp = Wireguard.generateKeypair();
  370. setV('settings.server.privateKey', kp.privateKey);
  371. setV('settings.server.publicKey', kp.publicKey);
  372. };
  373. // Randomizes the AmneziaWG 3.1 obfuscation set client-side; the shared
  374. // generator mirrors the Go backend's amneziawg.GenerateObfuscation31.
  375. const regenInboundAwgObfuscation = () => {
  376. const obf = generateAwgObfuscation();
  377. for (const [field, value] of Object.entries(obf)) {
  378. setV(`settings.server.${field}`, value);
  379. }
  380. };
  381. const matchesVlessAuth = (
  382. block: { id?: string; label?: string } | undefined | null,
  383. authId: string,
  384. ) => {
  385. if (block?.id === authId) return true;
  386. const label = (block?.label || '').toLowerCase().replace(/[-_\s]/g, '');
  387. if (authId === 'mlkem768')
  388. return label.includes('mlkem768') && !label.includes('xorpub') && !label.includes('random');
  389. if (authId === 'x25519')
  390. return label.includes('x25519') && !label.includes('xorpub') && !label.includes('random');
  391. if (authId === 'mlkem768_xorpub') return label.includes('mlkem768') && label.includes('xorpub');
  392. if (authId === 'mlkem768_random') return label.includes('mlkem768') && label.includes('random');
  393. if (authId === 'x25519_xorpub') return label.includes('x25519') && label.includes('xorpub');
  394. if (authId === 'x25519_random') return label.includes('x25519') && label.includes('random');
  395. return false;
  396. };
  397. const getNewVlessEnc = async (authId: string) => {
  398. if (!authId) return;
  399. setSaving(true);
  400. try {
  401. const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
  402. if (!msg?.success) return;
  403. const obj = msg.obj as {
  404. auths?: { decryption: string; encryption: string; label?: string; id?: string }[];
  405. };
  406. const block = (obj.auths || []).find((a) => matchesVlessAuth(a, authId));
  407. if (!block) return;
  408. setV('settings.decryption', block.decryption);
  409. setV('settings.encryption', block.encryption);
  410. } finally {
  411. setSaving(false);
  412. }
  413. };
  414. const clearVlessEnc = () => {
  415. setV('settings.decryption', 'none');
  416. setV('settings.encryption', 'none');
  417. };
  418. const vlessAuthKind = vlessEncryptionAuthKind(
  419. typeof vlessEncryption === 'string' ? vlessEncryption : '',
  420. );
  421. const selectedVlessAuth = (() => {
  422. const enc = typeof vlessEncryption === 'string' ? vlessEncryption : '';
  423. if (!enc || enc === 'none') return 'None';
  424. if (!vlessAuthKind) return t('pages.inbounds.vlessAuthCustom');
  425. return t(VLESS_AUTH_LABEL_KEYS[vlessAuthKind]);
  426. })();
  427. useEffect(() => {
  428. if (!open) return;
  429. const initial =
  430. mode === 'edit' && dbInbound
  431. ? withoutClients(rawInboundToFormValues(dbInbound))
  432. : buildAddModeValues();
  433. methods.reset(initial);
  434. setScanResult(null);
  435. setActiveTab('basic');
  436. const initialTag = (initial.tag ?? '') as string;
  437. autoTagRef.current = isAutoInboundTag(initialTag, {
  438. port: initial.port ?? 0,
  439. nodeId: initial.nodeId ?? null,
  440. protocol: initial.protocol,
  441. streamSettings: (initial.streamSettings ?? {}) as Record<string, unknown>,
  442. settings: (initial.settings ?? {}) as Record<string, unknown>,
  443. });
  444. lastWrittenTagRef.current = initialTag;
  445. if (
  446. mode === 'edit' &&
  447. dbInbound &&
  448. (dbInbound.protocol === Protocols.VLESS || dbInbound.protocol === Protocols.TROJAN)
  449. ) {
  450. loadFallbacks(dbInbound.id);
  451. } else {
  452. loadFallbacks(null);
  453. }
  454. /* eslint-disable-next-line react-hooks/exhaustive-deps */
  455. }, [open, mode, dbInbound, methods]);
  456. useEffect(() => {
  457. if (!open) return;
  458. if (wTag === lastWrittenTagRef.current) return;
  459. autoTagRef.current = isAutoInboundTag(wTag, currentTagInput());
  460. /* eslint-disable-next-line react-hooks/exhaustive-deps */
  461. }, [open, wTag]);
  462. useEffect(() => {
  463. if (!open || !autoTagRef.current) return;
  464. const next = composeInboundTag(currentTagInput());
  465. if (next !== ((getV('tag') as string | undefined) ?? '')) {
  466. lastWrittenTagRef.current = next;
  467. setV('tag', next);
  468. }
  469. /* eslint-disable-next-line react-hooks/exhaustive-deps */
  470. }, [open, wPort, wNodeId, protocol, network, mixedUdpOn, wSsNetwork, wTunnelNetwork]);
  471. /*
  472. * Keep the strategy value inside the visible option set: when `node` isn't
  473. * offered (no node, or a protocol that can't deploy to one) fall back to
  474. * `listen`, which yields the same link for a local inbound. Mirrors how the
  475. * protocol reset drops a nodeId that no longer applies.
  476. * Only downgrade once the inputs this decision depends on are settled, so a
  477. * persisted `node` strategy is never clobbered by transient mount state (#5375).
  478. */
  479. useEffect(() => {
  480. if (!open) return;
  481. if (!protocol) return;
  482. const current = getV('shareAddrStrategy') as InboundFormValues['shareAddrStrategy'] | undefined;
  483. if (protocol === Protocols.MTPROTO) {
  484. if (current !== 'listen') setV('shareAddrStrategy', 'listen');
  485. if (getV('shareAddr')) setV('shareAddr', '');
  486. return;
  487. }
  488. if (!availableNodesFetched) return;
  489. if (!nodeShareOptionAvailable && (current ?? 'node') === 'node') {
  490. setV('shareAddrStrategy', 'listen');
  491. }
  492. /* eslint-disable-next-line react-hooks/exhaustive-deps */
  493. }, [open, availableNodesFetched, protocol, nodeShareOptionAvailable, shareAddrStrategy]);
  494. /*
  495. * Protocol picker reset cascades through the form — clearing the settings DU
  496. * branch and dropping a nodeId that no longer applies. Only a real user
  497. * change (type === 'change') triggers it; programmatic setValue (advanced
  498. * JSON edits, open reset) must not, matching the legacy onValuesChange.
  499. */
  500. useEffect(() => {
  501. if (mode === 'edit') return;
  502. /* eslint-disable-next-line react-hooks/incompatible-library */
  503. const sub = methods.watch((_value, { name, type }) => {
  504. if (name !== 'protocol' || type !== 'change') return;
  505. const next = getV('protocol') as string;
  506. const settings = createDefaultInboundSettings(next) ?? undefined;
  507. setV('settings', settings);
  508. if (!NODE_ELIGIBLE_PROTOCOLS[next]) {
  509. setV('nodeId', null);
  510. }
  511. if (next !== Protocols.VLESS) {
  512. setV('disableFlow', false);
  513. }
  514. if (next === Protocols.HYSTERIA) {
  515. setV('streamSettings', {
  516. network: 'hysteria',
  517. security: 'tls',
  518. hysteriaSettings: HysteriaStreamSettingsSchema.parse({}),
  519. tlsSettings: createHysteriaTlsSettingsWithDefaultCert(),
  520. finalmask: {
  521. tcp: [],
  522. udp: [
  523. {
  524. type: 'salamander',
  525. settings: { password: RandomUtil.randomLowerAndNum(16) },
  526. },
  527. ],
  528. },
  529. });
  530. } else if (next === Protocols.MASQUE) {
  531. setV('streamSettings', {
  532. network: 'masque',
  533. security: 'tls',
  534. masqueSettings: { path: MASQUE_DEFAULT_PATH },
  535. tlsSettings: createMasqueTlsSettingsWithDefaultCert(),
  536. });
  537. } else if (next === Protocols.WIREGUARD || next === Protocols.TUNNEL) {
  538. setV('streamSettings', { security: 'none' });
  539. } else {
  540. const current = getV('streamSettings') as { network?: string } | undefined;
  541. if (current?.network === 'hysteria' || current?.network === 'masque' || !current?.network) {
  542. setV('streamSettings', { network: 'tcp', security: 'none', tcpSettings: {} });
  543. }
  544. }
  545. });
  546. return () => sub.unsubscribe();
  547. /* eslint-disable-next-line react-hooks/exhaustive-deps */
  548. }, [mode, methods]);
  549. const saveValues = async () => {
  550. // settings.fallbacks has no bound field; shouldUnregister=false keeps it from
  551. // the reset object. An edit sends no clients: the server keeps the stored ones.
  552. const values = methods.getValues() as InboundFormValues;
  553. const parsed = InboundFormSchema.safeParse(values);
  554. if (!parsed.success) {
  555. const issues = parsed.error.issues;
  556. setActiveTab(tabForValidationPath(issues[0].path));
  557. messageApi.error(formatInboundValidation(issues, values, t));
  558. console.error(
  559. '[InboundFormModal] schema validation failed:',
  560. issues.map((issue) => formatInboundIssue(issue, values, t)),
  561. );
  562. return;
  563. }
  564. setSaving(true);
  565. try {
  566. const payload = formValuesToWirePayload(parsed.data, { omitClients: mode === 'edit' });
  567. const url =
  568. mode === 'edit' && dbInbound
  569. ? `/panel/api/inbounds/update/${dbInbound.id}`
  570. : '/panel/api/inbounds/add';
  571. const msg = await HttpUtil.post(url, payload);
  572. if (msg?.success) {
  573. if (isFallbackHost) {
  574. const obj = msg.obj as { id?: number; Id?: number } | null;
  575. const masterId = mode === 'edit' ? dbInbound!.id : (obj?.id ?? obj?.Id ?? 0);
  576. if (masterId) await saveFallbacks(masterId);
  577. }
  578. onSaved();
  579. onClose();
  580. }
  581. } finally {
  582. setSaving(false);
  583. }
  584. };
  585. /*
  586. * Field errors render inline, but every tab is force-rendered, so an error on
  587. * a hidden tab looks like a dead Save button — jump to it and say what broke.
  588. */
  589. const submit = methods.handleSubmit(saveValues, (errors) => {
  590. const issue = firstRhfValidationIssue(errors);
  591. if (!issue) return;
  592. setActiveTab(tabForValidationPath(issue.path));
  593. messageApi.error(formatInboundIssue(issue, methods.getValues(), t));
  594. });
  595. const title =
  596. mode === 'edit' ? t('pages.inbounds.modifyInbound') : t('pages.inbounds.addInbound');
  597. const okText = mode === 'edit' ? t('pages.clients.submitEdit') : t('create');
  598. const basicTab = (
  599. <>
  600. {mode === 'add' && (
  601. <FormField name="enable" label={t('enable')} valueProp="checked">
  602. <Switch id="inbound-enable" />
  603. </FormField>
  604. )}
  605. <FormField name="remark" label={t('pages.inbounds.remark')}>
  606. <Input />
  607. </FormField>
  608. {selectableNodes.length > 0 && isNodeEligible && (
  609. <FormField name="nodeId" label={t('pages.inbounds.deployTo')}>
  610. <Select
  611. showSearch
  612. disabled={mode === 'edit'}
  613. placeholder={t('pages.inbounds.localPanel')}
  614. allowClear
  615. options={selectableNodes.map((n) => ({
  616. value: n.id,
  617. // Same rule as the clone target picker: only online is
  618. // deployable (`unknown` = no heartbeat yet).
  619. label: `${n.name}${n.status === 'online' ? '' : ` (${n.status || 'offline'})`}`,
  620. disabled: n.status !== 'online',
  621. }))}
  622. />
  623. </FormField>
  624. )}
  625. <FormField name="protocol" label={t('pages.inbounds.protocol')}>
  626. <Select id="protocol" disabled={mode === 'edit'} options={PROTOCOL_OPTIONS} />
  627. </FormField>
  628. <FormField
  629. name="listen"
  630. label={labelWithHint(t('pages.inbounds.address'), t('pages.inbounds.form.listenHelp'))}
  631. >
  632. <Input placeholder={t('pages.inbounds.monitorDesc')} />
  633. </FormField>
  634. {protocol !== Protocols.MTPROTO && (
  635. <>
  636. <FormField
  637. name="shareAddrStrategy"
  638. label={labelWithHint(
  639. t('pages.inbounds.form.shareAddrStrategy'),
  640. t('pages.inbounds.form.shareAddrStrategyHelp'),
  641. )}
  642. >
  643. <Select
  644. options={SHARE_ADDR_STRATEGIES.filter(
  645. (strategy) => strategy !== 'node' || nodeShareOptionAvailable,
  646. ).map((strategy) => ({
  647. value: strategy,
  648. label: t(`pages.inbounds.form.shareAddrStrategyOptions.${strategy}`),
  649. }))}
  650. />
  651. </FormField>
  652. {shareAddrStrategy === 'custom' && (
  653. <FormField
  654. name="shareAddr"
  655. label={labelWithHint(
  656. t('pages.inbounds.form.shareAddr'),
  657. t('pages.inbounds.form.shareAddrHelp'),
  658. )}
  659. rules={{
  660. validate: (value) =>
  661. isValidShareAddrInput(String(value ?? '')) ||
  662. t('pages.inbounds.form.shareAddrHelp'),
  663. }}
  664. >
  665. <Input placeholder="edge.example.com" />
  666. </FormField>
  667. )}
  668. </>
  669. )}
  670. <FormField
  671. name="subSortIndex"
  672. label={labelWithHint(
  673. t('pages.inbounds.form.subSortIndex'),
  674. t('pages.inbounds.form.subSortIndexHelp'),
  675. )}
  676. >
  677. <InputNumber />
  678. </FormField>
  679. <FormField
  680. name="excludeFromSub"
  681. valueProp="checked"
  682. label={labelWithHint(
  683. t('pages.inbounds.form.excludeFromSub'),
  684. t('pages.inbounds.form.excludeFromSubHelp'),
  685. )}
  686. >
  687. <Switch />
  688. </FormField>
  689. {protocol === Protocols.VLESS && (
  690. <FormField
  691. name="disableFlow"
  692. valueProp="checked"
  693. label={labelWithHint(
  694. t('pages.inbounds.form.disableFlow'),
  695. t('pages.inbounds.form.disableFlowHelp'),
  696. )}
  697. >
  698. <Switch />
  699. </FormField>
  700. )}
  701. <FormField
  702. name="port"
  703. label={t('pages.inbounds.port')}
  704. rules={{ validate: rhfZodValidate(InboundFormBaseSchema.shape.port) }}
  705. >
  706. <InputNumber min={isUdsListen ? 0 : 1} max={65535} />
  707. </FormField>
  708. <Form.Item
  709. label={
  710. <Tooltip title={t('pages.inbounds.meansNoLimit')}>
  711. {t('pages.inbounds.totalFlow')}
  712. </Tooltip>
  713. }
  714. >
  715. <InputNumber
  716. value={wTotal ? Math.round((wTotal / SizeFormatter.ONE_GB) * 100) / 100 : 0}
  717. min={0}
  718. step={1}
  719. onChange={(v) => {
  720. const bytes = NumberFormatter.toFixed((Number(v) || 0) * SizeFormatter.ONE_GB, 0);
  721. setV('total', bytes);
  722. }}
  723. />
  724. </Form.Item>
  725. <FormField name="trafficReset" label={t('pages.inbounds.periodicTrafficResetTitle')}>
  726. <Select
  727. options={TRAFFIC_RESETS.map((r) => ({
  728. value: r,
  729. label: t(`pages.inbounds.periodicTrafficReset.${r}`),
  730. }))}
  731. />
  732. </FormField>
  733. {trafficReset === 'monthly' && (
  734. <FormField
  735. name="trafficResetDay"
  736. label={t('pages.inbounds.periodicTrafficResetDay')}
  737. rules={{ validate: rhfZodValidate(InboundDbFieldsSchema.shape.trafficResetDay) }}
  738. >
  739. <InputNumber min={1} max={31} />
  740. </FormField>
  741. )}
  742. <Form.Item
  743. label={
  744. <Tooltip title={t('pages.inbounds.leaveBlankToNeverExpire')}>
  745. {t('pages.inbounds.expireDate')}
  746. </Tooltip>
  747. }
  748. >
  749. <DateTimePicker
  750. value={wExpiry > 0 ? dayjs(wExpiry) : null}
  751. onChange={(d) => setV('expiryTime', d ? d.valueOf() : 0)}
  752. />
  753. </Form.Item>
  754. </>
  755. );
  756. const fallbacksCard = (
  757. <FallbacksCard
  758. fallbacks={fallbacks}
  759. fallbackChildOptions={fallbackChildOptions}
  760. addFallback={addFallback}
  761. updateFallback={updateFallback}
  762. removeFallback={removeFallback}
  763. moveFallback={moveFallback}
  764. addAllFallbacks={addAllFallbacks}
  765. />
  766. );
  767. const protocolTab = (
  768. <>
  769. {protocol === Protocols.WIREGUARD && (
  770. <WireguardFields wgPubKey={wgPubKey} regenInboundWg={regenInboundWg} />
  771. )}
  772. {protocol === Protocols.AMNEZIAWG && (
  773. <AmneziawgFields
  774. awgPubKey={awgPubKey}
  775. regenInboundAwg={regenInboundAwg}
  776. regenInboundAwgObfuscation={regenInboundAwgObfuscation}
  777. />
  778. )}
  779. {protocol === Protocols.TUIC && <TuicFields />}
  780. {protocol === Protocols.MASQUE && <MasqueSettingsFields />}
  781. {protocol === Protocols.TUN && <TunFields />}
  782. {protocol === Protocols.TUNNEL && <TunnelFields />}
  783. {protocol === Protocols.HTTP && <HttpFields />}
  784. {protocol === Protocols.MIXED && <MixedFields mixedUdpOn={mixedUdpOn} />}
  785. {protocol === Protocols.MTPROTO && <MtprotoFields />}
  786. {protocol === Protocols.SHADOWSOCKS && <ShadowsocksFields isSSWith2022={isSSWith2022} />}
  787. {protocol === Protocols.VLESS && (
  788. <VlessFields
  789. saving={saving}
  790. selectedVlessAuth={selectedVlessAuth}
  791. vlessAuthKind={vlessAuthKind}
  792. network={network}
  793. security={security}
  794. getNewVlessEnc={getNewVlessEnc}
  795. clearVlessEnc={clearVlessEnc}
  796. />
  797. )}
  798. {isFallbackHost && fallbacksCard}
  799. {(protocol === Protocols.VLESS || protocol === Protocols.TROJAN) &&
  800. network === 'tcp' &&
  801. !isFallbackHost && (
  802. <Alert
  803. className="mt-12"
  804. type="info"
  805. showIcon
  806. title={t('pages.inbounds.fallbacks.needsTls')}
  807. />
  808. )}
  809. </>
  810. );
  811. /*
  812. * Switching `network` swaps which per-network key appears on the wire. Clear
  813. * the old network's blob and seed the new one with schema defaults, plus the
  814. * FinalMask mkcp-legacy UDP mask when moving to mKCP (removed otherwise).
  815. */
  816. const onNetworkChange = (next: string) => {
  817. const ALL = [
  818. 'tcpSettings',
  819. 'kcpSettings',
  820. 'wsSettings',
  821. 'grpcSettings',
  822. 'httpupgradeSettings',
  823. 'xhttpSettings',
  824. 'xdriveSettings',
  825. ];
  826. const current = (getV('streamSettings') as Record<string, unknown>) ?? {};
  827. const cleaned: Record<string, unknown> = { ...current, network: next };
  828. for (const k of ALL) {
  829. if (k !== `${next}Settings`) delete cleaned[k];
  830. }
  831. if (next === 'xdrive') {
  832. /* The core would read the inbound's TLS as the storage API's client TLS. */
  833. cleaned.security = 'none';
  834. delete cleaned.tlsSettings;
  835. delete cleaned.realitySettings;
  836. } else {
  837. delete cleaned.address;
  838. delete cleaned.port;
  839. }
  840. cleaned[`${next}Settings`] = newStreamSlice(next);
  841. if (next === 'kcp') {
  842. const fm = (cleaned.finalmask as Record<string, unknown> | undefined) ?? {};
  843. const udp = Array.isArray(fm.udp) ? (fm.udp as unknown[]) : [];
  844. const hasMkcp = udp.some((m) => {
  845. const entry = m as { type?: string };
  846. return entry?.type === 'mkcp-legacy';
  847. });
  848. if (!hasMkcp) {
  849. cleaned.finalmask = {
  850. ...fm,
  851. udp: [...udp, { type: 'mkcp-legacy', settings: { header: '', value: '' } }],
  852. };
  853. }
  854. } else {
  855. const fm = cleaned.finalmask as Record<string, unknown> | undefined;
  856. if (fm && Array.isArray(fm.udp)) {
  857. const udp = (fm.udp as unknown[]).filter(
  858. (m) => (m as { type?: string })?.type !== 'mkcp-legacy',
  859. );
  860. cleaned.finalmask = { ...fm, udp };
  861. }
  862. }
  863. /* WebSocket needs HTTP/1.1 for its handshake; the TLS default
  864. * ALPN ['h2','http/1.1'] makes the server negotiate h2 (see #6782). */
  865. if (next === 'ws' && cleaned.security === 'tls') {
  866. const tls = (cleaned.tlsSettings as Record<string, unknown> | undefined) ?? {};
  867. cleaned.tlsSettings = { ...tls, alpn: ['http/1.1'] };
  868. }
  869. setV('streamSettings', cleaned);
  870. };
  871. const streamTab = (
  872. <>
  873. {hasSelectableTransport && (
  874. <Form.Item label={t('transmission')}>
  875. <Select
  876. style={{ width: '75%' }}
  877. value={network}
  878. onChange={onNetworkChange}
  879. options={[
  880. { value: 'tcp', label: 'RAW' },
  881. { value: 'kcp', label: 'mKCP' },
  882. { value: 'ws', label: 'WebSocket' },
  883. { value: 'grpc', label: 'gRPC' },
  884. { value: 'httpupgrade', label: 'HTTPUpgrade' },
  885. { value: 'xhttp', label: 'XHTTP' },
  886. { value: 'xdrive', label: 'XDRIVE' },
  887. ]}
  888. />
  889. </Form.Item>
  890. )}
  891. {/* Inbound Hysteria stream sub-form. The transport for hysteria
  892. isn't user-selectable (always 'hysteria'), so the network
  893. dropdown is hidden above. */}
  894. {protocol === Protocols.HYSTERIA && <HysteriaFields />}
  895. {protocol === Protocols.MASQUE && <MasqueStreamFields />}
  896. {hasSelectableTransport && (
  897. <>
  898. {network === 'tcp' && <RawForm />}
  899. {network === 'ws' && <WsForm />}
  900. {network === 'grpc' && <GrpcForm />}
  901. {network === 'xhttp' && <XhttpForm />}
  902. {network === 'httpupgrade' && <HttpUpgradeForm />}
  903. {network === 'kcp' && <KcpForm />}
  904. {network === 'xdrive' && <XDriveForm />}
  905. </>
  906. )}
  907. {/* The legacy externalProxy section is replaced by the Hosts page; the
  908. field is still parsed/rendered for backward compatibility but is no
  909. longer editable here. */}
  910. <SockoptForm toggleSockopt={toggleSockopt} network={network} />
  911. {/* Transport masks don't apply to tunnel (a transparent forwarder), so
  912. its stream tab is just sockopt + TProxy. */}
  913. {protocol !== Protocols.TUNNEL && (
  914. <Controller
  915. control={control}
  916. name="streamSettings.finalmask"
  917. render={({ field }) => (
  918. <FinalMaskField
  919. key={`${protocol}:${network}`}
  920. value={field.value}
  921. onChange={field.onChange}
  922. network={network}
  923. protocol={protocol}
  924. side="server"
  925. />
  926. )}
  927. />
  928. )}
  929. </>
  930. );
  931. const tlsOk = canEnableTls({ protocol, streamSettings: { network, security } });
  932. const realityOk = canEnableReality({ protocol, streamSettings: { network, security } });
  933. const tlsOnly = protocol === Protocols.HYSTERIA || protocol === Protocols.MASQUE;
  934. const securityTab = (
  935. <>
  936. <Form.Item label={t('pages.inbounds.securityTab')}>
  937. <Radio.Group
  938. value={security}
  939. buttonStyle="solid"
  940. disabled={!tlsOk}
  941. onChange={(e) => onSecurityChange(e.target.value)}
  942. >
  943. {!tlsOnly && <Radio.Button value="none">{t('none')}</Radio.Button>}
  944. <Radio.Button value="tls">TLS</Radio.Button>
  945. {realityOk && <Radio.Button value="reality">Reality</Radio.Button>}
  946. </Radio.Group>
  947. </Form.Item>
  948. {security === 'tls' && (
  949. <TlsForm
  950. saving={saving}
  951. setCertFromPanel={setCertFromPanel}
  952. clearCertFiles={clearCertFiles}
  953. pinFromCert={pinFromCert}
  954. pinFromRemote={pinFromRemote}
  955. getNewEchCert={getNewEchCert}
  956. clearEchCert={clearEchCert}
  957. />
  958. )}
  959. {security === 'reality' && (
  960. <RealityForm
  961. saving={saving}
  962. scanning={scanning}
  963. scanResult={scanResult}
  964. scanRealityTarget={scanRealityTarget}
  965. scanRealityCandidates={scanRealityCandidates}
  966. applyRealityScanResult={applyRealityScanResult}
  967. randomizeShortIds={randomizeShortIds}
  968. randomizeSpiderX={randomizeSpiderX}
  969. genRealityKeypair={genRealityKeypair}
  970. clearRealityKeypair={clearRealityKeypair}
  971. genMldsa65={genMldsa65}
  972. clearMldsa65={clearMldsa65}
  973. />
  974. )}
  975. </>
  976. );
  977. const advancedTab = (
  978. <div className="advanced-shell">
  979. <div className="advanced-panel">
  980. <div className="advanced-panel__header">
  981. <div>
  982. <div className="advanced-panel__title">{t('pages.inbounds.advanced.title')}</div>
  983. <div className="advanced-panel__subtitle">{t('pages.inbounds.advanced.subtitle')}</div>
  984. </div>
  985. </div>
  986. <Tabs
  987. className="advanced-inner-tabs"
  988. items={[
  989. {
  990. key: 'all',
  991. label: t('pages.inbounds.advanced.all'),
  992. children: (
  993. <>
  994. <div className="advanced-editor-meta">{t('pages.inbounds.advanced.allHelp')}</div>
  995. <AdvancedAllEditor
  996. streamEnabled={streamEnabled}
  997. sniffingEnabled={sniffingSupported}
  998. />
  999. </>
  1000. ),
  1001. },
  1002. {
  1003. key: 'settings',
  1004. label: t('pages.inbounds.advanced.settings'),
  1005. children: (
  1006. <>
  1007. <div className="advanced-editor-meta">
  1008. {t('pages.inbounds.advanced.settingsHelp')}{' '}
  1009. <code>{'{ settings: { ... } }'}</code>.
  1010. </div>
  1011. <AdvancedSliceEditor
  1012. path="settings"
  1013. wrapKey="settings"
  1014. minHeight="320px"
  1015. maxHeight="540px"
  1016. />
  1017. </>
  1018. ),
  1019. },
  1020. ...(streamEnabled
  1021. ? [
  1022. {
  1023. key: 'stream',
  1024. label: t('pages.inbounds.advanced.stream'),
  1025. children: (
  1026. <>
  1027. <div className="advanced-editor-meta">
  1028. {t('pages.inbounds.advanced.streamHelp')}{' '}
  1029. <code>{'{ streamSettings: { ... } }'}</code>.
  1030. </div>
  1031. <AdvancedSliceEditor
  1032. path="streamSettings"
  1033. wrapKey="streamSettings"
  1034. minHeight="320px"
  1035. maxHeight="540px"
  1036. />
  1037. </>
  1038. ),
  1039. },
  1040. ]
  1041. : []),
  1042. ...(sniffingSupported
  1043. ? [
  1044. {
  1045. key: 'sniffing',
  1046. label: t('pages.inbounds.advanced.sniffing'),
  1047. children: (
  1048. <>
  1049. <div className="advanced-editor-meta">
  1050. {t('pages.inbounds.advanced.sniffingHelp')}{' '}
  1051. <code>{'{ sniffing: { ... } }'}</code>.
  1052. </div>
  1053. <AdvancedSliceEditor
  1054. path="sniffing"
  1055. wrapKey="sniffing"
  1056. minHeight="240px"
  1057. maxHeight="420px"
  1058. />
  1059. </>
  1060. ),
  1061. },
  1062. ]
  1063. : []),
  1064. ]}
  1065. />
  1066. </div>
  1067. </div>
  1068. );
  1069. const sniffingTab = <SniffingTab />;
  1070. return (
  1071. <>
  1072. {messageContextHolder}
  1073. {modalContextHolder}
  1074. <Modal
  1075. open={open}
  1076. title={title}
  1077. okText={okText}
  1078. cancelText={t('close')}
  1079. confirmLoading={saving}
  1080. mask={{ closable: false }}
  1081. width={780}
  1082. onOk={submit}
  1083. onCancel={onClose}
  1084. destroyOnHidden
  1085. >
  1086. <FormProvider {...methods}>
  1087. <Form
  1088. colon={false}
  1089. labelCol={{ sm: { span: 8 } }}
  1090. wrapperCol={{ sm: { span: 14 } }}
  1091. labelWrap
  1092. >
  1093. <Tabs
  1094. activeKey={activeTab}
  1095. onChange={setActiveTab}
  1096. items={[
  1097. {
  1098. key: 'basic',
  1099. label: t('pages.xray.basicTemplate'),
  1100. children: basicTab,
  1101. forceRender: true,
  1102. },
  1103. ...((
  1104. [
  1105. Protocols.VLESS,
  1106. Protocols.SHADOWSOCKS,
  1107. Protocols.HTTP,
  1108. Protocols.MIXED,
  1109. Protocols.TUNNEL,
  1110. Protocols.TUN,
  1111. Protocols.WIREGUARD,
  1112. Protocols.MTPROTO,
  1113. Protocols.AMNEZIAWG,
  1114. Protocols.TUIC,
  1115. Protocols.MASQUE,
  1116. ] as string[]
  1117. ).includes(protocol) || isFallbackHost
  1118. ? [
  1119. {
  1120. key: 'protocol',
  1121. label: t('pages.inbounds.protocol'),
  1122. children: protocolTab,
  1123. forceRender: true,
  1124. },
  1125. ]
  1126. : []),
  1127. ...(streamEnabled
  1128. ? [
  1129. {
  1130. key: 'stream',
  1131. label: t('pages.inbounds.streamTab'),
  1132. children: streamTab,
  1133. forceRender: true,
  1134. },
  1135. ...(protocol !== Protocols.WIREGUARD && protocol !== Protocols.TUNNEL
  1136. ? [
  1137. {
  1138. key: 'security',
  1139. label: t('pages.inbounds.securityTab'),
  1140. children: securityTab,
  1141. forceRender: true,
  1142. },
  1143. ]
  1144. : []),
  1145. ]
  1146. : []),
  1147. ...(sniffingSupported
  1148. ? [
  1149. {
  1150. key: 'sniffing',
  1151. label: t('pages.inbounds.sniffingTab'),
  1152. children: sniffingTab,
  1153. forceRender: true,
  1154. },
  1155. ]
  1156. : []),
  1157. {
  1158. key: 'advanced',
  1159. label: t('pages.xray.advancedTemplate'),
  1160. children: advancedTab,
  1161. forceRender: true,
  1162. },
  1163. ]}
  1164. />
  1165. </Form>
  1166. </FormProvider>
  1167. </Modal>
  1168. </>
  1169. );
  1170. }