InboundFormModal.tsx 106 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262
  1. import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import dayjs, { type Dayjs } from 'dayjs';
  4. import {
  5. Button,
  6. Card,
  7. Checkbox,
  8. Col,
  9. Divider,
  10. Empty,
  11. Form,
  12. Input,
  13. InputNumber,
  14. Modal,
  15. Radio,
  16. Row,
  17. Select,
  18. Space,
  19. Switch,
  20. Tabs,
  21. Tooltip,
  22. Typography,
  23. message,
  24. } from 'antd';
  25. import {
  26. SyncOutlined,
  27. PlusOutlined,
  28. MinusOutlined,
  29. DeleteOutlined,
  30. CaretUpOutlined,
  31. CaretDownOutlined,
  32. SettingOutlined,
  33. } from '@ant-design/icons';
  34. import {
  35. HttpUtil,
  36. RandomUtil,
  37. NumberFormatter,
  38. SizeFormatter,
  39. Wireguard,
  40. } from '@/utils';
  41. import InputAddon from '@/components/InputAddon';
  42. import { getRandomRealityTarget } from '@/models/reality-targets';
  43. import {
  44. Inbound,
  45. Protocols,
  46. SSMethods,
  47. SNIFFING_OPTION,
  48. TLS_VERSION_OPTION,
  49. TLS_CIPHER_OPTION,
  50. UTLS_FINGERPRINT,
  51. ALPN_OPTION,
  52. USAGE_OPTION,
  53. DOMAIN_STRATEGY_OPTION,
  54. TCP_CONGESTION_OPTION,
  55. MODE_OPTION,
  56. } from '@/models/inbound';
  57. import { DBInbound } from '@/models/dbinbound';
  58. import FinalMaskForm from '@/components/FinalMaskForm';
  59. import DateTimePicker from '@/components/DateTimePicker';
  60. import JsonEditor from '@/components/JsonEditor';
  61. import type { NodeRecord } from '@/api/queries/useNodesQuery';
  62. import { InboundFormSchema } from '@/schemas/inbound';
  63. import './InboundFormModal.css';
  64. const { TextArea } = Input;
  65. const { Text, Paragraph } = Typography;
  66. interface InboundFormModalProps {
  67. open: boolean;
  68. onClose: () => void;
  69. onSaved: () => void;
  70. mode: 'add' | 'edit';
  71. dbInbound: DBInbound | null;
  72. dbInbounds: DBInbound[];
  73. availableNodes?: NodeRecord[];
  74. }
  75. interface StreamLike {
  76. network?: string;
  77. tcp?: { type?: string; request?: { path?: string[] }; acceptProxyProtocol?: boolean };
  78. ws?: { path?: string; acceptProxyProtocol?: boolean };
  79. grpc?: { serviceName?: string; multiMode?: boolean };
  80. httpupgrade?: { path?: string; acceptProxyProtocol?: boolean };
  81. xhttp?: { path?: string };
  82. security?: string;
  83. tls?: { certs?: TlsCert[] };
  84. reality?: unknown;
  85. externalProxy?: unknown;
  86. }
  87. interface TlsCert {
  88. useFile?: boolean;
  89. certFile?: string;
  90. keyFile?: string;
  91. cert?: string;
  92. key?: string;
  93. ocspStapling?: number;
  94. oneTimeLoading?: boolean;
  95. usage?: string;
  96. buildChain?: boolean;
  97. }
  98. interface VlessClient {
  99. id?: string;
  100. email?: string;
  101. flow?: string;
  102. enable?: boolean;
  103. subId?: string;
  104. totalGB?: number;
  105. expiryTime?: number;
  106. limitIp?: number;
  107. comment?: string;
  108. tgId?: string;
  109. }
  110. interface ShadowsocksClient {
  111. email?: string;
  112. password?: string;
  113. method?: string;
  114. enable?: boolean;
  115. subId?: string;
  116. totalGB?: number;
  117. expiryTime?: number;
  118. limitIp?: number;
  119. comment?: string;
  120. tgId?: string;
  121. }
  122. interface HttpAccount {
  123. user?: string;
  124. pass?: string;
  125. }
  126. interface WireguardPeer {
  127. privateKey?: string;
  128. publicKey?: string;
  129. psk?: string;
  130. allowedIPs: string[];
  131. keepAlive?: number;
  132. }
  133. const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'];
  134. const PROTOCOLS = Object.values(Protocols) as string[];
  135. const TLS_VERSIONS = Object.values(TLS_VERSION_OPTION) as string[];
  136. const CIPHER_SUITES = Object.entries(TLS_CIPHER_OPTION) as [string, string][];
  137. const FINGERPRINTS = Object.values(UTLS_FINGERPRINT) as string[];
  138. const ALPNS = Object.values(ALPN_OPTION) as string[];
  139. const USAGES = Object.values(USAGE_OPTION) as string[];
  140. const DOMAIN_STRATEGIES = Object.values(DOMAIN_STRATEGY_OPTION) as string[];
  141. const TCP_CONGESTIONS = Object.values(TCP_CONGESTION_OPTION) as string[];
  142. const MODE_OPTIONS = Object.values(MODE_OPTION) as string[];
  143. const NODE_ELIGIBLE_PROTOCOLS = new Set([
  144. Protocols.VLESS,
  145. Protocols.VMESS,
  146. Protocols.TROJAN,
  147. Protocols.SHADOWSOCKS,
  148. Protocols.HYSTERIA,
  149. Protocols.WIREGUARD,
  150. ]);
  151. const FALLBACK_ELIGIBLE_TRANSPORTS = new Set(['tcp', 'ws', 'grpc', 'httpupgrade', 'xhttp']);
  152. interface FallbackRow {
  153. rowKey: string;
  154. childId: number | null;
  155. name: string;
  156. alpn: string;
  157. path: string;
  158. xver: number;
  159. }
  160. function deriveFallbackDefaults(childDb: DBInbound | null | undefined): Omit<FallbackRow, 'rowKey' | 'childId'> {
  161. const out = { name: '', alpn: '', path: '', xver: 0 };
  162. if (!childDb) return out;
  163. let stream: StreamLike | undefined;
  164. try {
  165. stream = childDb.toInbound()?.stream as StreamLike | undefined;
  166. } catch {
  167. return out;
  168. }
  169. if (!stream) return out;
  170. switch (stream.network) {
  171. case 'tcp': {
  172. const tcp = stream.tcp;
  173. if (tcp?.type === 'http') {
  174. const p = tcp?.request?.path;
  175. if (Array.isArray(p) && p.length) out.path = p[0];
  176. }
  177. if (tcp?.acceptProxyProtocol) out.xver = 2;
  178. break;
  179. }
  180. case 'ws': {
  181. out.path = stream.ws?.path || '';
  182. if (stream.ws?.acceptProxyProtocol) out.xver = 2;
  183. break;
  184. }
  185. case 'grpc': {
  186. out.path = stream.grpc?.serviceName || '';
  187. out.alpn = 'h2';
  188. break;
  189. }
  190. case 'httpupgrade': {
  191. out.path = stream.httpupgrade?.path || '';
  192. if (stream.httpupgrade?.acceptProxyProtocol) out.xver = 2;
  193. break;
  194. }
  195. case 'xhttp': {
  196. out.path = stream.xhttp?.path || '';
  197. break;
  198. }
  199. }
  200. return out;
  201. }
  202. export default function InboundFormModal({
  203. open,
  204. onClose,
  205. onSaved,
  206. mode,
  207. dbInbound,
  208. dbInbounds,
  209. availableNodes,
  210. }: InboundFormModalProps) {
  211. const { t } = useTranslation();
  212. const [messageApi, messageContextHolder] = message.useMessage();
  213. const selectableNodes = useMemo(
  214. () => (availableNodes || []).filter((n: NodeRecord) => n.enable),
  215. [availableNodes],
  216. );
  217. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  218. const inboundRef = useRef<any>(null);
  219. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  220. const dbFormRef = useRef<any>(null);
  221. const fallbackKeyRef = useRef(0);
  222. const advancedTextRef = useRef({ stream: '', sniffing: '', settings: '' });
  223. const [, setTick] = useState(0);
  224. const refresh = useCallback(() => setTick((n) => n + 1), []);
  225. const [saving, setSaving] = useState(false);
  226. const [activeTabKey, setActiveTabKey] = useState('basic');
  227. const [advancedSectionKey, setAdvancedSectionKey] = useState('all');
  228. const [defaultCert, setDefaultCert] = useState('');
  229. const [defaultKey, setDefaultKey] = useState('');
  230. const [fallbacks, setFallbacks] = useState<FallbackRow[]>([]);
  231. const [fallbackEditing, setFallbackEditing] = useState<Set<string>>(new Set());
  232. const isVlessLike = inboundRef.current?.protocol === Protocols.VLESS;
  233. const isFallbackHost = useMemo(() => {
  234. const ib = inboundRef.current;
  235. if (!ib) return false;
  236. if (ib.protocol !== Protocols.VLESS && ib.protocol !== Protocols.TROJAN) return false;
  237. if (ib.stream?.network !== 'tcp') return false;
  238. const sec = ib.stream?.security;
  239. return sec === 'tls' || sec === 'reality';
  240. // eslint-disable-next-line react-hooks/exhaustive-deps
  241. }, [inboundRef.current?.protocol, inboundRef.current?.stream?.network, inboundRef.current?.stream?.security]);
  242. const canEnableStream = inboundRef.current?.canEnableStream?.() === true;
  243. const canEnableTls = inboundRef.current?.canEnableTls?.() === true;
  244. const canEnableReality = inboundRef.current?.canEnableReality?.() === true;
  245. const isNodeEligible = NODE_ELIGIBLE_PROTOCOLS.has(inboundRef.current?.protocol);
  246. const hasProtocolTabContent = useMemo(() => {
  247. const ib = inboundRef.current;
  248. if (!ib) return false;
  249. if (ib.protocol === Protocols.VLESS) return true;
  250. if (isFallbackHost) return true;
  251. switch (ib.protocol) {
  252. case Protocols.SHADOWSOCKS:
  253. case Protocols.HTTP:
  254. case Protocols.MIXED:
  255. case Protocols.TUNNEL:
  256. case Protocols.TUN:
  257. case Protocols.WIREGUARD:
  258. return true;
  259. default:
  260. return false;
  261. }
  262. // eslint-disable-next-line react-hooks/exhaustive-deps
  263. }, [inboundRef.current?.protocol, isFallbackHost]);
  264. const externalProxyOn = Array.isArray(inboundRef.current?.stream?.externalProxy)
  265. && inboundRef.current.stream.externalProxy.length > 0;
  266. const stampAdvancedTextFor = useCallback((slice: 'stream' | 'sniffing' | 'settings') => {
  267. const ib = inboundRef.current;
  268. if (!ib) return;
  269. if (slice === 'stream' && !ib.canEnableStream?.()) {
  270. advancedTextRef.current.stream = '{}';
  271. return;
  272. }
  273. const obj = ib[slice];
  274. if (!obj) return;
  275. try {
  276. advancedTextRef.current[slice] = JSON.stringify(JSON.parse(obj.toString()), null, 2);
  277. } catch {
  278. /* keep prior */
  279. }
  280. }, []);
  281. const primeAdvancedJson = useCallback(() => {
  282. (['stream', 'sniffing', 'settings'] as const).forEach(stampAdvancedTextFor);
  283. }, [stampAdvancedTextFor]);
  284. const loadFallbacks = useCallback(async (masterId: number | null) => {
  285. if (!masterId) {
  286. setFallbacks([]);
  287. return;
  288. }
  289. const msg = await HttpUtil.get(`/panel/api/inbounds/${masterId}/fallbacks`);
  290. if (!msg?.success || !Array.isArray(msg.obj)) {
  291. setFallbacks([]);
  292. return;
  293. }
  294. setFallbacks(
  295. (msg.obj as { childId: number; name?: string; alpn?: string; path?: string; xver?: number }[]).map((r) => ({
  296. rowKey: `fb-${++fallbackKeyRef.current}`,
  297. childId: r.childId,
  298. name: r.name || '',
  299. alpn: r.alpn || '',
  300. path: r.path || '',
  301. xver: r.xver || 0,
  302. })),
  303. );
  304. }, []);
  305. const fetchDefaultCertSettings = useCallback(async () => {
  306. try {
  307. const msg = await HttpUtil.post('/panel/setting/defaultSettings');
  308. if (msg?.success && msg.obj) {
  309. const obj = msg.obj as { defaultCert?: string; defaultKey?: string };
  310. setDefaultCert(obj.defaultCert || '');
  311. setDefaultKey(obj.defaultKey || '');
  312. }
  313. } catch {
  314. /* non-fatal */
  315. }
  316. }, []);
  317. useEffect(() => {
  318. if (!open) return;
  319. setFallbackEditing(new Set());
  320. if (mode === 'edit' && dbInbound) {
  321. const parsed = Inbound.fromJson(dbInbound.toInbound().toJson());
  322. inboundRef.current = parsed;
  323. dbFormRef.current = new DBInbound(dbInbound);
  324. primeAdvancedJson();
  325. if (dbInbound.protocol === Protocols.VLESS || dbInbound.protocol === Protocols.TROJAN) {
  326. loadFallbacks(dbInbound.id);
  327. } else {
  328. setFallbacks([]);
  329. }
  330. } else {
  331. const ib = new Inbound();
  332. ib.protocol = Protocols.VLESS;
  333. ib.settings = Inbound.Settings.getSettings(Protocols.VLESS);
  334. ib.port = RandomUtil.randomInteger(10000, 60000);
  335. inboundRef.current = ib;
  336. const form = new DBInbound();
  337. form.enable = true;
  338. form.remark = '';
  339. form.total = 0;
  340. form.expiryTime = 0;
  341. form.trafficReset = 'never';
  342. dbFormRef.current = form;
  343. primeAdvancedJson();
  344. setFallbacks([]);
  345. }
  346. setActiveTabKey('basic');
  347. setAdvancedSectionKey('all');
  348. fetchDefaultCertSettings();
  349. refresh();
  350. }, [open, mode, dbInbound, primeAdvancedJson, loadFallbacks, fetchDefaultCertSettings, refresh]);
  351. const setExternalProxy = useCallback((on: boolean) => {
  352. const ib = inboundRef.current;
  353. if (!ib?.stream) return;
  354. if (on) {
  355. ib.stream.externalProxy = [{
  356. forceTls: 'same',
  357. dest: window.location.hostname,
  358. port: ib.port,
  359. remark: '',
  360. sni: '',
  361. fingerprint: '',
  362. alpn: [],
  363. }];
  364. } else {
  365. ib.stream.externalProxy = [];
  366. }
  367. refresh();
  368. }, [refresh]);
  369. const onProtocolChange = useCallback((next: string) => {
  370. const ib = inboundRef.current;
  371. if (mode === 'edit' || !ib) return;
  372. ib.protocol = next;
  373. ib.settings = Inbound.Settings.getSettings(next);
  374. if (!NODE_ELIGIBLE_PROTOCOLS.has(next) && dbFormRef.current) {
  375. dbFormRef.current.nodeId = null;
  376. }
  377. primeAdvancedJson();
  378. refresh();
  379. }, [mode, primeAdvancedJson, refresh]);
  380. const onNetworkChange = useCallback((next: string) => {
  381. const ib = inboundRef.current;
  382. if (!ib?.stream) return;
  383. ib.stream.network = next;
  384. if (!ib.canEnableTls()) ib.stream.security = 'none';
  385. if (!ib.canEnableReality()) ib.reality = false;
  386. if (
  387. ib.protocol === Protocols.VLESS
  388. && !ib.canEnableTlsFlow()
  389. && Array.isArray(ib.settings.vlesses)
  390. ) {
  391. ib.settings.vlesses.forEach((c: VlessClient) => { c.flow = ''; });
  392. }
  393. if (next !== 'kcp' && ib.stream.finalmask) {
  394. ib.stream.finalmask.udp = [];
  395. }
  396. stampAdvancedTextFor('stream');
  397. refresh();
  398. }, [stampAdvancedTextFor, refresh]);
  399. const setSecurity = useCallback((v: string) => {
  400. const ib = inboundRef.current;
  401. if (ib?.stream) {
  402. ib.stream.security = v;
  403. refresh();
  404. }
  405. }, [refresh]);
  406. const addFallback = useCallback((childId: number | null = null) => {
  407. const row: FallbackRow = {
  408. rowKey: `fb-${++fallbackKeyRef.current}`,
  409. childId: childId || null,
  410. name: '',
  411. alpn: '',
  412. path: '',
  413. xver: 0,
  414. };
  415. if (childId) {
  416. const child = (dbInbounds || []).find((ib) => ib.id === childId);
  417. Object.assign(row, deriveFallbackDefaults(child));
  418. }
  419. setFallbacks((prev) => [...prev, row]);
  420. }, [dbInbounds]);
  421. const removeFallback = useCallback((idx: number) => {
  422. setFallbacks((prev) => prev.filter((_, i) => i !== idx));
  423. }, []);
  424. const moveFallback = useCallback((idx: number, dir: number) => {
  425. setFallbacks((prev) => {
  426. const arr = [...prev];
  427. const j = idx + dir;
  428. if (j < 0 || j >= arr.length) return prev;
  429. [arr[idx], arr[j]] = [arr[j], arr[idx]];
  430. return arr;
  431. });
  432. }, []);
  433. const onFallbackChildPicked = useCallback((rowKey: string, childId: number) => {
  434. setFallbacks((prev) => prev.map((row) => {
  435. if (row.rowKey !== rowKey) return row;
  436. const child = (dbInbounds || []).find((ib) => ib.id === childId);
  437. const defaults = deriveFallbackDefaults(child);
  438. return { ...row, childId, ...defaults };
  439. }));
  440. }, [dbInbounds]);
  441. const updateFallback = useCallback((rowKey: string, patch: Partial<FallbackRow>) => {
  442. setFallbacks((prev) => prev.map((row) => (row.rowKey === rowKey ? { ...row, ...patch } : row)));
  443. }, []);
  444. const rederiveFallback = useCallback((rowKey: string) => {
  445. setFallbacks((prev) => prev.map((row) => {
  446. if (row.rowKey !== rowKey || !row.childId) return row;
  447. const child = (dbInbounds || []).find((ib) => ib.id === row.childId);
  448. const defaults = deriveFallbackDefaults(child);
  449. return { ...row, ...defaults };
  450. }));
  451. messageApi.success(t('pages.inbounds.fallbacks.rederived') || 'Re-filled from child');
  452. }, [dbInbounds, t, messageApi]);
  453. const quickAddAllFallbacks = useCallback(() => {
  454. const masterId = dbInbound?.id;
  455. const list = dbInbounds || [];
  456. setFallbacks((prev) => {
  457. const existing = new Set(prev.map((r) => r.childId).filter(Boolean));
  458. const next = [...prev];
  459. let added = 0;
  460. for (const ib of list) {
  461. if (ib.id === masterId) continue;
  462. if (existing.has(ib.id)) continue;
  463. let stream: StreamLike | undefined;
  464. try { stream = ib.toInbound()?.stream as StreamLike | undefined; } catch { continue; }
  465. if (!stream || !FALLBACK_ELIGIBLE_TRANSPORTS.has(stream.network ?? '')) continue;
  466. const row: FallbackRow = {
  467. rowKey: `fb-${++fallbackKeyRef.current}`,
  468. childId: ib.id,
  469. ...deriveFallbackDefaults(ib),
  470. };
  471. next.push(row);
  472. added += 1;
  473. }
  474. if (added > 0) {
  475. messageApi.success(t('pages.inbounds.fallbacks.quickAdded', { n: added }) || `Added ${added} fallback(s)`);
  476. } else {
  477. messageApi.info(t('pages.inbounds.fallbacks.quickAddedNone') || 'No new eligible inbounds to add');
  478. }
  479. return next;
  480. });
  481. }, [dbInbound, dbInbounds, t, messageApi]);
  482. const fallbackChildOptions = useMemo(() => {
  483. const list = dbInbounds || [];
  484. const masterId = dbInbound?.id;
  485. return list
  486. .filter((ib) => ib.id !== masterId)
  487. .map((ib) => ({
  488. label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`,
  489. value: ib.id,
  490. }));
  491. }, [dbInbounds, dbInbound]);
  492. const toggleFallbackEdit = useCallback((rowKey: string) => {
  493. setFallbackEditing((prev) => {
  494. const next = new Set(prev);
  495. if (next.has(rowKey)) next.delete(rowKey); else next.add(rowKey);
  496. return next;
  497. });
  498. }, []);
  499. const describeFallback = useCallback((record: FallbackRow) => {
  500. const parts: string[] = [];
  501. if (record.name) parts.push(`SNI=${record.name}`);
  502. if (record.alpn) parts.push(`ALPN=${record.alpn}`);
  503. if (record.path) parts.push(`path=${record.path}`);
  504. const condition = parts.length
  505. ? `${t('pages.inbounds.fallbacks.routesWhen') || 'Routes when'} ${parts.join(' · ')}`
  506. : (t('pages.inbounds.fallbacks.defaultCatchAll') || 'Default — catches anything else');
  507. const proxyTag = record.xver === 2 ? ' · PROXY v2' : record.xver === 1 ? ' · PROXY v1' : '';
  508. return { condition, proxyTag };
  509. }, [t]);
  510. const withSaving = useCallback(async <T,>(fn: () => Promise<T>): Promise<T> => {
  511. setSaving(true);
  512. try { return await fn(); } finally { setSaving(false); }
  513. }, []);
  514. const randomSSPassword = useCallback((target: ShadowsocksClient) => {
  515. if (target) {
  516. target.password = RandomUtil.randomShadowsocksPassword(inboundRef.current.settings.method);
  517. refresh();
  518. }
  519. }, [refresh]);
  520. const regenWgKeypair = useCallback((target: WireguardPeer) => {
  521. const kp = Wireguard.generateKeypair();
  522. target.publicKey = kp.publicKey;
  523. target.privateKey = kp.privateKey;
  524. refresh();
  525. }, [refresh]);
  526. const regenInboundWg = useCallback(() => {
  527. const kp = Wireguard.generateKeypair();
  528. inboundRef.current.settings.pubKey = kp.publicKey;
  529. inboundRef.current.settings.secretKey = kp.privateKey;
  530. refresh();
  531. }, [refresh]);
  532. const genRealityKeypair = useCallback(async () => {
  533. await withSaving(async () => {
  534. const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
  535. if (msg?.success) {
  536. const obj = msg.obj as { privateKey: string; publicKey: string };
  537. inboundRef.current.stream.reality.privateKey = obj.privateKey;
  538. inboundRef.current.stream.reality.settings.publicKey = obj.publicKey;
  539. refresh();
  540. }
  541. });
  542. }, [withSaving, refresh]);
  543. const clearRealityKeypair = useCallback(() => {
  544. if (!inboundRef.current?.stream?.reality) return;
  545. inboundRef.current.stream.reality.privateKey = '';
  546. inboundRef.current.stream.reality.settings.publicKey = '';
  547. refresh();
  548. }, [refresh]);
  549. const genMldsa65 = useCallback(async () => {
  550. await withSaving(async () => {
  551. const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
  552. if (msg?.success) {
  553. const obj = msg.obj as { seed: string; verify: string };
  554. inboundRef.current.stream.reality.mldsa65Seed = obj.seed;
  555. inboundRef.current.stream.reality.settings.mldsa65Verify = obj.verify;
  556. refresh();
  557. }
  558. });
  559. }, [withSaving, refresh]);
  560. const clearMldsa65 = useCallback(() => {
  561. if (!inboundRef.current?.stream?.reality) return;
  562. inboundRef.current.stream.reality.mldsa65Seed = '';
  563. inboundRef.current.stream.reality.settings.mldsa65Verify = '';
  564. refresh();
  565. }, [refresh]);
  566. const randomizeRealityTarget = useCallback(() => {
  567. if (!inboundRef.current?.stream?.reality) return;
  568. const target = getRandomRealityTarget() as { target: string; sni: string };
  569. inboundRef.current.stream.reality.target = target.target;
  570. inboundRef.current.stream.reality.serverNames = target.sni;
  571. refresh();
  572. }, [refresh]);
  573. const randomizeShortIds = useCallback(() => {
  574. if (!inboundRef.current?.stream?.reality) return;
  575. inboundRef.current.stream.reality.shortIds = RandomUtil.randomShortIds();
  576. refresh();
  577. }, [refresh]);
  578. const getNewEchCert = useCallback(async () => {
  579. if (!inboundRef.current?.stream?.tls) return;
  580. await withSaving(async () => {
  581. const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', {
  582. sni: inboundRef.current.stream.tls.sni,
  583. });
  584. if (msg?.success) {
  585. const obj = msg.obj as { echServerKeys: string; echConfigList: string };
  586. inboundRef.current.stream.tls.echServerKeys = obj.echServerKeys;
  587. inboundRef.current.stream.tls.settings.echConfigList = obj.echConfigList;
  588. refresh();
  589. }
  590. });
  591. }, [withSaving, refresh]);
  592. const clearEchCert = useCallback(() => {
  593. if (!inboundRef.current?.stream?.tls) return;
  594. inboundRef.current.stream.tls.echServerKeys = '';
  595. inboundRef.current.stream.tls.settings.echConfigList = '';
  596. refresh();
  597. }, [refresh]);
  598. const setDefaultCertData = useCallback((idx: number) => {
  599. if (!inboundRef.current?.stream?.tls?.certs?.[idx]) return;
  600. inboundRef.current.stream.tls.certs[idx].certFile = defaultCert;
  601. inboundRef.current.stream.tls.certs[idx].keyFile = defaultKey;
  602. refresh();
  603. }, [defaultCert, defaultKey, refresh]);
  604. const matchesVlessAuth = useCallback((block: { id?: string; label?: string } | undefined | null, authId: string) => {
  605. if (block?.id === authId) return true;
  606. const label = (block?.label || '').toLowerCase().replace(/[-_\s]/g, '');
  607. if (authId === 'mlkem768') return label.includes('mlkem768');
  608. if (authId === 'x25519') return label.includes('x25519');
  609. return false;
  610. }, []);
  611. const getNewVlessEnc = useCallback(async (authId: string) => {
  612. if (!authId || !inboundRef.current?.settings) return;
  613. await withSaving(async () => {
  614. const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
  615. if (!msg?.success) return;
  616. const obj = msg.obj as { auths?: { decryption: string; encryption: string; label?: string; id?: string }[] };
  617. const block = (obj.auths || []).find((a) => matchesVlessAuth(a, authId));
  618. if (!block) return;
  619. inboundRef.current.settings.decryption = block.decryption;
  620. inboundRef.current.settings.encryption = block.encryption;
  621. refresh();
  622. });
  623. }, [withSaving, refresh, matchesVlessAuth]);
  624. const clearVlessEnc = useCallback(() => {
  625. if (!inboundRef.current?.settings) return;
  626. inboundRef.current.settings.decryption = 'none';
  627. inboundRef.current.settings.encryption = 'none';
  628. refresh();
  629. }, [refresh]);
  630. const selectedVlessAuth = useMemo(() => {
  631. const encryption = inboundRef.current?.settings?.encryption;
  632. if (!encryption || encryption === 'none') return 'None';
  633. const parts = encryption.split('.').filter(Boolean);
  634. const authKey = parts[parts.length - 1] || '';
  635. if (!authKey) return t('pages.inbounds.vlessAuthCustom');
  636. return authKey.length > 300
  637. ? t('pages.inbounds.vlessAuthMlkem768')
  638. : t('pages.inbounds.vlessAuthX25519');
  639. // eslint-disable-next-line react-hooks/exhaustive-deps
  640. }, [inboundRef.current?.settings?.encryption, t]);
  641. const onSSMethodChange = useCallback(() => {
  642. const ib = inboundRef.current;
  643. ib.settings.password = RandomUtil.randomShadowsocksPassword(ib.settings.method);
  644. if (ib.isSSMultiUser) {
  645. ib.settings.shadowsockses.forEach((c: ShadowsocksClient) => {
  646. c.method = ib.isSS2022 ? '' : ib.settings.method;
  647. c.password = RandomUtil.randomShadowsocksPassword(ib.settings.method);
  648. });
  649. } else {
  650. ib.settings.shadowsockses = [];
  651. }
  652. refresh();
  653. }, [refresh]);
  654. const parseAdvancedSliceOrFallback = (rawText: string, fallback: unknown) => {
  655. if (!rawText?.trim()) return fallback;
  656. return JSON.parse(rawText);
  657. };
  658. const settingsFallback = () => inboundRef.current?.settings?.toJson?.() || {};
  659. const sniffingFallback = () => inboundRef.current?.sniffing?.toJson?.() || {};
  660. const streamFallback = () => inboundRef.current?.stream?.toJson?.() || {};
  661. const parseAdvancedSliceWithLabel = useCallback((rawText: string, fallback: unknown, label: string) => {
  662. try {
  663. return parseAdvancedSliceOrFallback(rawText, fallback);
  664. } catch (e) {
  665. messageApi.error(`${label} JSON invalid: ${(e as Error).message}`);
  666. throw e;
  667. }
  668. }, [messageApi]);
  669. const compactAdvancedJson = useCallback((raw: string, fallback: string, label: string) => {
  670. try {
  671. return JSON.stringify(JSON.parse(raw || fallback));
  672. } catch (e) {
  673. messageApi.error(`${label} JSON invalid: ${(e as Error).message}`);
  674. throw e;
  675. }
  676. }, [messageApi]);
  677. const applyAdvancedJsonToBasic = useCallback((): boolean => {
  678. const ib = inboundRef.current;
  679. if (!ib) return true;
  680. let settings: unknown;
  681. let streamSettings: unknown;
  682. let sniffing: unknown;
  683. try {
  684. settings = parseAdvancedSliceWithLabel(advancedTextRef.current.settings, settingsFallback(), t('pages.inbounds.advanced.settings'));
  685. streamSettings = parseAdvancedSliceWithLabel(advancedTextRef.current.stream, streamFallback(), t('pages.inbounds.advanced.stream'));
  686. sniffing = parseAdvancedSliceWithLabel(advancedTextRef.current.sniffing, sniffingFallback(), t('pages.inbounds.advanced.sniffing'));
  687. } catch {
  688. return false;
  689. }
  690. try {
  691. inboundRef.current = Inbound.fromJson({
  692. port: ib.port,
  693. listen: ib.listen,
  694. protocol: ib.protocol,
  695. settings,
  696. streamSettings,
  697. tag: ib.tag,
  698. sniffing,
  699. clientStats: ib.clientStats,
  700. });
  701. refresh();
  702. } catch (e) {
  703. messageApi.error(`${t('pages.inbounds.advanced.jsonErrorPrefix')}: ${(e as Error).message}`);
  704. return false;
  705. }
  706. return true;
  707. }, [t, refresh, parseAdvancedSliceWithLabel, messageApi]);
  708. const handleTabChange = (next: string) => {
  709. if (document.activeElement instanceof HTMLElement) {
  710. document.activeElement.blur();
  711. }
  712. if (activeTabKey === 'advanced' && next !== 'advanced') {
  713. if (!applyAdvancedJsonToBasic()) return;
  714. }
  715. setActiveTabKey(next);
  716. };
  717. const unwrapWrappedObject = (parsed: unknown, key: string): unknown => {
  718. if (
  719. parsed
  720. && typeof parsed === 'object'
  721. && !Array.isArray(parsed)
  722. && (parsed as Record<string, unknown>)[key] !== undefined
  723. ) {
  724. return (parsed as Record<string, unknown>)[key];
  725. }
  726. return parsed;
  727. };
  728. const wrappedConfigValue = (key: string, slice: 'stream' | 'sniffing' | 'settings'): string => {
  729. const ib = inboundRef.current;
  730. if (!ib) return '';
  731. try {
  732. const fb = slice === 'settings' ? settingsFallback() : slice === 'sniffing' ? sniffingFallback() : streamFallback();
  733. const value = parseAdvancedSliceOrFallback(advancedTextRef.current[slice], fb);
  734. return JSON.stringify({ [key]: value }, null, 2);
  735. } catch {
  736. return '';
  737. }
  738. };
  739. const setWrappedConfigValue = (key: string, slice: 'stream' | 'sniffing' | 'settings', label: string, next: string) => {
  740. let parsed: unknown;
  741. try {
  742. parsed = JSON.parse(next);
  743. } catch (e) {
  744. messageApi.error(`${label} JSON invalid: ${(e as Error).message}`);
  745. return;
  746. }
  747. const unwrapped = unwrapWrappedObject(parsed, key);
  748. if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
  749. messageApi.error(`${label} JSON must be an object or { ${key}: { ... } }.`);
  750. return;
  751. }
  752. try {
  753. advancedTextRef.current[slice] = JSON.stringify(unwrapped, null, 2);
  754. refresh();
  755. } catch (e) {
  756. messageApi.error(`${label} JSON invalid: ${(e as Error).message}`);
  757. }
  758. };
  759. const advancedAllValue = (() => {
  760. const ib = inboundRef.current;
  761. if (!ib) return '';
  762. try {
  763. const result: Record<string, unknown> = {
  764. listen: ib.listen,
  765. port: ib.port,
  766. protocol: ib.protocol,
  767. settings: parseAdvancedSliceOrFallback(advancedTextRef.current.settings, settingsFallback()),
  768. sniffing: parseAdvancedSliceOrFallback(advancedTextRef.current.sniffing, sniffingFallback()),
  769. tag: ib.tag,
  770. };
  771. if (canEnableStream) {
  772. result.streamSettings = parseAdvancedSliceOrFallback(advancedTextRef.current.stream, streamFallback());
  773. }
  774. return JSON.stringify(result, null, 2);
  775. } catch {
  776. return '';
  777. }
  778. })();
  779. const setAdvancedAllValue = (next: string) => {
  780. let parsedRaw: unknown;
  781. try {
  782. parsedRaw = JSON.parse(next);
  783. } catch (e) {
  784. messageApi.error(`All JSON invalid: ${(e as Error).message}`);
  785. return;
  786. }
  787. if (!parsedRaw || typeof parsedRaw !== 'object' || Array.isArray(parsedRaw)) {
  788. messageApi.error('All JSON must be an inbound object.');
  789. return;
  790. }
  791. const parsed = parsedRaw as {
  792. listen?: string;
  793. port?: number | string;
  794. protocol?: string;
  795. tag?: string;
  796. settings?: unknown;
  797. sniffing?: unknown;
  798. streamSettings?: unknown;
  799. };
  800. const ib = inboundRef.current;
  801. try {
  802. if (typeof parsed.listen === 'string') ib.listen = parsed.listen;
  803. if (parsed.port !== undefined) {
  804. const port = Number(parsed.port);
  805. if (Number.isFinite(port)) ib.port = port;
  806. }
  807. if (typeof parsed.protocol === 'string' && PROTOCOLS.includes(parsed.protocol)) {
  808. ib.protocol = parsed.protocol;
  809. }
  810. if (typeof parsed.tag === 'string') ib.tag = parsed.tag;
  811. const existingSettings = parseAdvancedSliceOrFallback(advancedTextRef.current.settings, settingsFallback());
  812. advancedTextRef.current.settings = JSON.stringify(parsed.settings ?? existingSettings, null, 2);
  813. advancedTextRef.current.sniffing = JSON.stringify(parsed.sniffing ?? sniffingFallback(), null, 2);
  814. advancedTextRef.current.stream = canEnableStream
  815. ? JSON.stringify(parsed.streamSettings ?? streamFallback(), null, 2)
  816. : '{}';
  817. refresh();
  818. } catch (e) {
  819. messageApi.error(`All JSON invalid: ${(e as Error).message}`);
  820. }
  821. };
  822. const saveFallbacks = useCallback(async (masterId: number) => {
  823. if (!masterId) return true;
  824. const payload = {
  825. fallbacks: fallbacks
  826. .filter((c) => c.childId)
  827. .map((c, i) => ({
  828. childId: c.childId,
  829. name: c.name,
  830. alpn: c.alpn,
  831. path: c.path,
  832. xver: Number(c.xver) || 0,
  833. sortOrder: i,
  834. })),
  835. };
  836. const msg = await HttpUtil.post(
  837. `/panel/api/inbounds/${masterId}/fallbacks`,
  838. payload,
  839. { headers: { 'Content-Type': 'application/json' } },
  840. );
  841. return !!msg?.success;
  842. }, [fallbacks]);
  843. const submit = useCallback(async () => {
  844. const ib = inboundRef.current;
  845. const form = dbFormRef.current;
  846. if (!ib || !form) return;
  847. setSaving(true);
  848. try {
  849. let streamSettings: string;
  850. let sniffing: string;
  851. let settings: string;
  852. try {
  853. streamSettings = canEnableStream
  854. ? compactAdvancedJson(advancedTextRef.current.stream, '', t('pages.inbounds.advanced.stream'))
  855. : (ib.stream?.sockopt
  856. ? JSON.stringify({ sockopt: ib.stream.sockopt.toJson() })
  857. : '');
  858. sniffing = compactAdvancedJson(advancedTextRef.current.sniffing, ib.sniffing.toString(), t('pages.inbounds.advanced.sniffing'));
  859. settings = compactAdvancedJson(advancedTextRef.current.settings, ib.settings.toString(), t('pages.inbounds.advanced.settings'));
  860. } catch { return; }
  861. const baseCheck = InboundFormSchema.safeParse({
  862. remark: form.remark ?? '',
  863. enable: !!form.enable,
  864. port: Number(ib.port),
  865. listen: ib.listen ?? '',
  866. protocol: ib.protocol ?? '',
  867. });
  868. if (!baseCheck.success) {
  869. const issue = baseCheck.error.issues[0];
  870. messageApi.error(t(issue?.message ?? 'somethingWentWrong', { defaultValue: issue?.message ?? 'invalid' }));
  871. return;
  872. }
  873. const payload: Record<string, unknown> = {
  874. up: form.up || 0,
  875. down: form.down || 0,
  876. total: form.total,
  877. remark: form.remark,
  878. enable: form.enable,
  879. expiryTime: form.expiryTime,
  880. trafficReset: form.trafficReset,
  881. lastTrafficResetTime: form.lastTrafficResetTime || 0,
  882. listen: ib.listen,
  883. port: ib.port,
  884. protocol: ib.protocol,
  885. settings,
  886. streamSettings,
  887. sniffing,
  888. };
  889. if (form.nodeId != null) payload.nodeId = form.nodeId;
  890. const url = mode === 'edit'
  891. ? `/panel/api/inbounds/update/${dbInbound!.id}`
  892. : '/panel/api/inbounds/add';
  893. const msg = await HttpUtil.post(url, payload);
  894. if (msg?.success) {
  895. if (isFallbackHost) {
  896. const obj = msg.obj as { id?: number; Id?: number } | null;
  897. const masterId = mode === 'edit'
  898. ? dbInbound!.id
  899. : (obj?.id || obj?.Id);
  900. if (masterId) await saveFallbacks(masterId);
  901. }
  902. onSaved();
  903. onClose();
  904. }
  905. } finally {
  906. setSaving(false);
  907. }
  908. }, [canEnableStream, compactAdvancedJson, t, messageApi, mode, dbInbound, isFallbackHost, saveFallbacks, onSaved, onClose]);
  909. const protocolSnapshot = inboundRef.current?.protocol;
  910. const streamSnapshot = JSON.stringify(inboundRef.current?.stream?.toJson?.() || {});
  911. const sniffingSnapshot = JSON.stringify(inboundRef.current?.sniffing?.toJson?.() || {});
  912. const settingsSnapshot = JSON.stringify(inboundRef.current?.settings?.toJson?.() || {});
  913. useEffect(() => {
  914. if (!inboundRef.current) return;
  915. (['stream', 'sniffing', 'settings'] as const).forEach(stampAdvancedTextFor);
  916. }, [protocolSnapshot, streamSnapshot, sniffingSnapshot, settingsSnapshot, stampAdvancedTextFor]);
  917. const title = mode === 'edit' ? t('pages.inbounds.modifyInbound') : t('pages.inbounds.addInbound');
  918. const okText = mode === 'edit' ? t('pages.clients.submitEdit') : t('create');
  919. const ib = inboundRef.current;
  920. const form = dbFormRef.current;
  921. if (!ib || !form) {
  922. return <Modal open={open} onCancel={onClose} title={title} footer={null} width={780} />;
  923. }
  924. const totalGB = form.total ? Math.round((form.total / SizeFormatter.ONE_GB) * 100) / 100 : 0;
  925. const expiryDate: Dayjs | null = form.expiryTime > 0 ? dayjs(form.expiryTime) : null;
  926. const renderBasicsTab = () => (
  927. <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
  928. <Form.Item label={t('enable')}>
  929. <Switch checked={!!form.enable} onChange={(v) => { form.enable = v; refresh(); }} />
  930. </Form.Item>
  931. <Form.Item label={t('pages.inbounds.remark')}>
  932. <Input value={form.remark} onChange={(e) => { form.remark = e.target.value; refresh(); }} />
  933. </Form.Item>
  934. {selectableNodes.length > 0 && isNodeEligible && (
  935. <Form.Item label={t('pages.inbounds.deployTo')}>
  936. <Select
  937. value={form.nodeId ?? ''}
  938. disabled={mode === 'edit'}
  939. placeholder={t('pages.inbounds.localPanel')}
  940. allowClear
  941. onChange={(v) => { form.nodeId = v === '' || v == null ? null : v; refresh(); }}
  942. >
  943. <Select.Option value="">{t('pages.inbounds.localPanel')}</Select.Option>
  944. {selectableNodes.map((n: NodeRecord) => (
  945. <Select.Option key={n.id} value={n.id} disabled={n.status === 'offline'}>
  946. {n.name}{n.status === 'offline' ? ' (offline)' : ''}
  947. </Select.Option>
  948. ))}
  949. </Select>
  950. </Form.Item>
  951. )}
  952. <Form.Item label={t('pages.inbounds.protocol')}>
  953. <Select
  954. value={ib.protocol}
  955. disabled={mode === 'edit'}
  956. onChange={onProtocolChange}
  957. >
  958. {PROTOCOLS.map((p) => <Select.Option key={p} value={p}>{p}</Select.Option>)}
  959. </Select>
  960. </Form.Item>
  961. <Form.Item label={t('pages.inbounds.address')}>
  962. <Input
  963. value={ib.listen}
  964. placeholder={t('pages.inbounds.monitorDesc')}
  965. onChange={(e) => { ib.listen = e.target.value; refresh(); }}
  966. />
  967. </Form.Item>
  968. <Form.Item label={t('pages.inbounds.port')}>
  969. <InputNumber
  970. value={ib.port}
  971. min={1}
  972. max={65535}
  973. onChange={(v) => { ib.port = Number(v) || 0; refresh(); }}
  974. />
  975. </Form.Item>
  976. <Form.Item label={<Tooltip title={t('pages.inbounds.meansNoLimit')}>{t('pages.inbounds.totalFlow')}</Tooltip>}>
  977. <InputNumber
  978. value={totalGB}
  979. min={0}
  980. step={1}
  981. onChange={(v) => {
  982. form.total = NumberFormatter.toFixed((Number(v) || 0) * SizeFormatter.ONE_GB, 0);
  983. refresh();
  984. }}
  985. />
  986. </Form.Item>
  987. <Form.Item label={t('pages.inbounds.periodicTrafficResetTitle')}>
  988. <Select value={form.trafficReset} onChange={(v) => { form.trafficReset = v; refresh(); }}>
  989. {TRAFFIC_RESETS.map((r) => (
  990. <Select.Option key={r} value={r}>{t(`pages.inbounds.periodicTrafficReset.${r}`)}</Select.Option>
  991. ))}
  992. </Select>
  993. </Form.Item>
  994. <Form.Item label={<Tooltip title={t('pages.inbounds.leaveBlankToNeverExpire')}>{t('pages.inbounds.expireDate')}</Tooltip>}>
  995. <DateTimePicker
  996. value={expiryDate}
  997. onChange={(d) => { form.expiryTime = d ? d.valueOf() : 0; refresh(); }}
  998. />
  999. </Form.Item>
  1000. </Form>
  1001. );
  1002. const renderFallbacksCard = () => (
  1003. <Card size="small" className="mt-12" title={t('pages.inbounds.fallbacks.title') || 'Fallbacks'}>
  1004. <Paragraph type="secondary" style={{ marginBottom: 12 }}>
  1005. {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.'}
  1006. </Paragraph>
  1007. {fallbacks.length === 0 && (
  1008. <Empty description={t('pages.inbounds.fallbacks.empty') || 'No fallbacks yet'} styles={{ image: { height: 40 } }} style={{ margin: '8px 0 12px' }} />
  1009. )}
  1010. {fallbacks.map((record, index) => (
  1011. <div key={record.rowKey} style={{ border: '1px solid var(--app-border-tertiary)', borderRadius: 6, padding: '10px 12px', marginBottom: 8 }}>
  1012. <Row gutter={8} align="middle" wrap={false}>
  1013. <Col flex="none">
  1014. <Space orientation="vertical" size={2}>
  1015. <Button size="small" type="text" disabled={index === 0} onClick={() => moveFallback(index, -1)}>
  1016. <CaretUpOutlined />
  1017. </Button>
  1018. <Button size="small" type="text" disabled={index === fallbacks.length - 1} onClick={() => moveFallback(index, 1)}>
  1019. <CaretDownOutlined />
  1020. </Button>
  1021. </Space>
  1022. </Col>
  1023. <Col flex="auto">
  1024. <Select
  1025. value={record.childId}
  1026. options={fallbackChildOptions}
  1027. showSearch
  1028. placeholder={t('pages.inbounds.fallbacks.pickInbound') || 'Pick an inbound'}
  1029. filterOption={(input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase())}
  1030. style={{ width: '100%' }}
  1031. onChange={(v) => onFallbackChildPicked(record.rowKey, v)}
  1032. />
  1033. <Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
  1034. {describeFallback(record).condition}{describeFallback(record).proxyTag}
  1035. </Text>
  1036. </Col>
  1037. <Col flex="none">
  1038. <Space size={4}>
  1039. <Tooltip title={t('pages.inbounds.fallbacks.rederive') || 'Re-fill from child'}>
  1040. <Button size="small" type="text" disabled={!record.childId} onClick={() => rederiveFallback(record.rowKey)}>
  1041. <SyncOutlined />
  1042. </Button>
  1043. </Tooltip>
  1044. <Tooltip title={fallbackEditing.has(record.rowKey)
  1045. ? (t('pages.inbounds.fallbacks.hideAdvanced') || 'Hide advanced')
  1046. : (t('pages.inbounds.fallbacks.editAdvanced') || 'Edit routing fields')}>
  1047. <Button size="small" type="text" onClick={() => toggleFallbackEdit(record.rowKey)}>
  1048. <SettingOutlined />
  1049. </Button>
  1050. </Tooltip>
  1051. <Button size="small" type="text" danger onClick={() => removeFallback(index)}>
  1052. <DeleteOutlined />
  1053. </Button>
  1054. </Space>
  1055. </Col>
  1056. </Row>
  1057. {fallbackEditing.has(record.rowKey) && (
  1058. <Row gutter={8} style={{ marginTop: 8 }}>
  1059. <Col xs={24} md={8}>
  1060. <Space.Compact block>
  1061. <InputAddon>SNI</InputAddon>
  1062. <Input placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
  1063. value={record.name} onChange={(e) => updateFallback(record.rowKey, { name: e.target.value })} />
  1064. </Space.Compact>
  1065. </Col>
  1066. <Col xs={24} md={5}>
  1067. <Space.Compact block>
  1068. <InputAddon>ALPN</InputAddon>
  1069. <Input placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
  1070. value={record.alpn} onChange={(e) => updateFallback(record.rowKey, { alpn: e.target.value })} />
  1071. </Space.Compact>
  1072. </Col>
  1073. <Col xs={24} md={7}>
  1074. <Space.Compact block>
  1075. <InputAddon>Path</InputAddon>
  1076. <Input placeholder="/" value={record.path}
  1077. onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })} />
  1078. </Space.Compact>
  1079. </Col>
  1080. <Col xs={24} md={4}>
  1081. <Space.Compact block>
  1082. <InputAddon>xver</InputAddon>
  1083. <InputNumber min={0} max={2} style={{ width: '100%' }}
  1084. value={record.xver}
  1085. onChange={(v) => updateFallback(record.rowKey, { xver: Number(v) || 0 })} />
  1086. </Space.Compact>
  1087. </Col>
  1088. </Row>
  1089. )}
  1090. </div>
  1091. ))}
  1092. <Space size={8} style={{ marginTop: 4 }} wrap>
  1093. <Button size="small" onClick={() => addFallback()}>
  1094. <PlusOutlined /> {t('pages.inbounds.fallbacks.add') || 'Add fallback'}
  1095. </Button>
  1096. <Button size="small" type="primary" ghost onClick={quickAddAllFallbacks}>
  1097. {t('pages.inbounds.fallbacks.quickAddAll') || 'Quick add all eligible'}
  1098. </Button>
  1099. </Space>
  1100. </Card>
  1101. );
  1102. const renderProtocolTab = () => (
  1103. <>
  1104. {isVlessLike && (
  1105. <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
  1106. <Form.Item label={t('pages.inbounds.decryption')}>
  1107. <Input value={ib.settings.decryption} onChange={(e) => { ib.settings.decryption = e.target.value; refresh(); }} />
  1108. </Form.Item>
  1109. <Form.Item label={t('pages.inbounds.encryption')}>
  1110. <Input value={ib.settings.encryption} onChange={(e) => { ib.settings.encryption = e.target.value; refresh(); }} />
  1111. </Form.Item>
  1112. <Form.Item label=" ">
  1113. <Space size={8} wrap>
  1114. <Button type="primary" loading={saving} onClick={() => getNewVlessEnc('x25519')}>
  1115. {t('pages.inbounds.vlessAuthX25519')}
  1116. </Button>
  1117. <Button type="primary" loading={saving} onClick={() => getNewVlessEnc('mlkem768')}>
  1118. {t('pages.inbounds.vlessAuthMlkem768')}
  1119. </Button>
  1120. <Button danger onClick={clearVlessEnc}>{t('clear')}</Button>
  1121. </Space>
  1122. <Text type="secondary" className="vless-auth-state">
  1123. {t('pages.inbounds.vlessAuthSelected', { auth: selectedVlessAuth })}
  1124. </Text>
  1125. </Form.Item>
  1126. </Form>
  1127. )}
  1128. {isFallbackHost && renderFallbacksCard()}
  1129. {ib.protocol === Protocols.SHADOWSOCKS && (
  1130. <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
  1131. <Form.Item label="Encryption method">
  1132. <Select value={ib.settings.method} onChange={(v) => { ib.settings.method = v; onSSMethodChange(); }}>
  1133. {Object.entries(SSMethods).map(([k, m]) => (
  1134. <Select.Option key={k} value={m as string}>{k}</Select.Option>
  1135. ))}
  1136. </Select>
  1137. </Form.Item>
  1138. {ib.isSS2022 && (
  1139. <Form.Item label={<>Password <SyncOutlined className="random-icon" onClick={() => randomSSPassword(ib.settings)} /></>}>
  1140. <Input value={ib.settings.password} onChange={(e) => { ib.settings.password = e.target.value; refresh(); }} />
  1141. </Form.Item>
  1142. )}
  1143. <Form.Item label="Network">
  1144. <Select value={ib.settings.network} style={{ width: 120 }} onChange={(v) => { ib.settings.network = v; refresh(); }}>
  1145. <Select.Option value="tcp,udp">TCP, UDP</Select.Option>
  1146. <Select.Option value="tcp">TCP</Select.Option>
  1147. <Select.Option value="udp">UDP</Select.Option>
  1148. </Select>
  1149. </Form.Item>
  1150. <Form.Item label="ivCheck">
  1151. <Switch checked={!!ib.settings.ivCheck} onChange={(v) => { ib.settings.ivCheck = v; refresh(); }} />
  1152. </Form.Item>
  1153. </Form>
  1154. )}
  1155. {(ib.protocol === Protocols.HTTP || ib.protocol === Protocols.MIXED) && (
  1156. <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
  1157. <Form.Item label="Accounts">
  1158. <Button size="small" onClick={() => {
  1159. const Account = ib.protocol === Protocols.HTTP
  1160. ? Inbound.HttpSettings.HttpAccount
  1161. : Inbound.MixedSettings.SocksAccount;
  1162. ib.settings.addAccount(new Account());
  1163. refresh();
  1164. }}>
  1165. <PlusOutlined /> Add
  1166. </Button>
  1167. </Form.Item>
  1168. <Form.Item wrapperCol={{ span: 24 }}>
  1169. {(ib.settings.accounts || []).map((account: HttpAccount, idx: number) => (
  1170. <Space.Compact key={idx} className="mb-8" block>
  1171. <InputAddon>{String(idx + 1)}</InputAddon>
  1172. <Input value={account.user} placeholder="Username"
  1173. onChange={(e) => { account.user = e.target.value; refresh(); }} />
  1174. <Input value={account.pass} placeholder="Password"
  1175. onChange={(e) => { account.pass = e.target.value; refresh(); }} />
  1176. <Button onClick={() => { ib.settings.delAccount(idx); refresh(); }}>
  1177. <MinusOutlined />
  1178. </Button>
  1179. </Space.Compact>
  1180. ))}
  1181. </Form.Item>
  1182. {ib.protocol === Protocols.HTTP && (
  1183. <Form.Item label="Allow transparent">
  1184. <Switch checked={!!ib.settings.allowTransparent} onChange={(v) => { ib.settings.allowTransparent = v; refresh(); }} />
  1185. </Form.Item>
  1186. )}
  1187. {ib.protocol === Protocols.MIXED && (
  1188. <>
  1189. <Form.Item label="Auth">
  1190. <Select value={ib.settings.auth} onChange={(v) => { ib.settings.auth = v; refresh(); }}>
  1191. <Select.Option value="noauth">noauth</Select.Option>
  1192. <Select.Option value="password">password</Select.Option>
  1193. </Select>
  1194. </Form.Item>
  1195. <Form.Item label="UDP">
  1196. <Switch checked={!!ib.settings.udp} onChange={(v) => { ib.settings.udp = v; refresh(); }} />
  1197. </Form.Item>
  1198. {ib.settings.udp && (
  1199. <Form.Item label="UDP IP">
  1200. <Input value={ib.settings.ip} onChange={(e) => { ib.settings.ip = e.target.value; refresh(); }} />
  1201. </Form.Item>
  1202. )}
  1203. </>
  1204. )}
  1205. </Form>
  1206. )}
  1207. {ib.protocol === Protocols.TUNNEL && (
  1208. <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
  1209. <Form.Item label="Rewrite address">
  1210. <Input value={ib.settings.rewriteAddress} onChange={(e) => { ib.settings.rewriteAddress = e.target.value; refresh(); }} />
  1211. </Form.Item>
  1212. <Form.Item label="Rewrite port">
  1213. <InputNumber value={ib.settings.rewritePort} min={0} max={65535}
  1214. onChange={(v) => { ib.settings.rewritePort = Number(v) || 0; refresh(); }} />
  1215. </Form.Item>
  1216. <Form.Item label="Allowed network">
  1217. <Select value={ib.settings.allowedNetwork} onChange={(v) => { ib.settings.allowedNetwork = v; refresh(); }}>
  1218. <Select.Option value="tcp,udp">TCP, UDP</Select.Option>
  1219. <Select.Option value="tcp">TCP</Select.Option>
  1220. <Select.Option value="udp">UDP</Select.Option>
  1221. </Select>
  1222. </Form.Item>
  1223. <Form.Item label="Port map">
  1224. <Button size="small" onClick={() => { ib.settings.addPortMap('', ''); refresh(); }}>
  1225. <PlusOutlined />
  1226. </Button>
  1227. </Form.Item>
  1228. {(ib.settings.portMap || []).length > 0 && (
  1229. <Form.Item wrapperCol={{ span: 24 }}>
  1230. {(ib.settings.portMap as { name: string; value: string }[]).map((pm, idx) => (
  1231. <Space.Compact key={`pm-${idx}`} className="mb-8" block>
  1232. <InputAddon>{String(idx + 1)}</InputAddon>
  1233. <Input value={pm.name} placeholder="5555"
  1234. onChange={(e) => { pm.name = e.target.value; refresh(); }} />
  1235. <Input value={pm.value} placeholder="1.1.1.1:7777"
  1236. onChange={(e) => { pm.value = e.target.value; refresh(); }} />
  1237. <Button onClick={() => { ib.settings.removePortMap(idx); refresh(); }}>
  1238. <MinusOutlined />
  1239. </Button>
  1240. </Space.Compact>
  1241. ))}
  1242. </Form.Item>
  1243. )}
  1244. <Form.Item label="Follow redirect">
  1245. <Switch checked={!!ib.settings.followRedirect} onChange={(v) => { ib.settings.followRedirect = v; refresh(); }} />
  1246. </Form.Item>
  1247. </Form>
  1248. )}
  1249. {ib.protocol === Protocols.TUN && (
  1250. <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
  1251. <Form.Item label="Interface name">
  1252. <Input value={ib.settings.name} placeholder="xray0"
  1253. onChange={(e) => { ib.settings.name = e.target.value; refresh(); }} />
  1254. </Form.Item>
  1255. <Form.Item label="MTU">
  1256. <InputNumber value={ib.settings.mtu} min={0}
  1257. onChange={(v) => { ib.settings.mtu = Number(v) || 0; refresh(); }} />
  1258. </Form.Item>
  1259. <Form.Item label="Gateway">
  1260. <Button size="small" onClick={() => { ib.settings.gateway.push(''); refresh(); }}>
  1261. <PlusOutlined />
  1262. </Button>
  1263. {(ib.settings.gateway || []).map((_ip: string, j: number) => (
  1264. <Space.Compact key={`tun-gw-${j}`} block className="mt-4">
  1265. <Input
  1266. placeholder={j === 0 ? '10.0.0.1/16' : 'fc00::1/64'}
  1267. value={ib.settings.gateway[j]}
  1268. onChange={(e) => { ib.settings.gateway[j] = e.target.value; refresh(); }} />
  1269. <Button size="small" onClick={() => { ib.settings.gateway.splice(j, 1); refresh(); }}>
  1270. <MinusOutlined />
  1271. </Button>
  1272. </Space.Compact>
  1273. ))}
  1274. </Form.Item>
  1275. <Form.Item label="DNS">
  1276. <Button size="small" onClick={() => { ib.settings.dns.push(''); refresh(); }}>
  1277. <PlusOutlined />
  1278. </Button>
  1279. {(ib.settings.dns || []).map((_ip: string, j: number) => (
  1280. <Space.Compact key={`tun-dns-${j}`} block className="mt-4">
  1281. <Input
  1282. placeholder={j === 0 ? '1.1.1.1' : '8.8.8.8'}
  1283. value={ib.settings.dns[j]}
  1284. onChange={(e) => { ib.settings.dns[j] = e.target.value; refresh(); }} />
  1285. <Button size="small" onClick={() => { ib.settings.dns.splice(j, 1); refresh(); }}>
  1286. <MinusOutlined />
  1287. </Button>
  1288. </Space.Compact>
  1289. ))}
  1290. </Form.Item>
  1291. <Form.Item label="User level">
  1292. <InputNumber value={ib.settings.userLevel} min={0}
  1293. onChange={(v) => { ib.settings.userLevel = Number(v) || 0; refresh(); }} />
  1294. </Form.Item>
  1295. <Form.Item label={<Tooltip title="Windows-only. CIDRs added to the system routing table automatically so matching traffic goes through TUN.">Auto system routes</Tooltip>}>
  1296. <Button size="small" onClick={() => { ib.settings.autoSystemRoutingTable.push(''); refresh(); }}>
  1297. <PlusOutlined />
  1298. </Button>
  1299. {(ib.settings.autoSystemRoutingTable || []).map((_ip: string, j: number) => (
  1300. <Space.Compact key={`tun-rt-${j}`} block className="mt-4">
  1301. <Input
  1302. placeholder={j === 0 ? '0.0.0.0/0' : '::/0'}
  1303. value={ib.settings.autoSystemRoutingTable[j]}
  1304. onChange={(e) => { ib.settings.autoSystemRoutingTable[j] = e.target.value; refresh(); }} />
  1305. <Button size="small" onClick={() => { ib.settings.autoSystemRoutingTable.splice(j, 1); refresh(); }}>
  1306. <MinusOutlined />
  1307. </Button>
  1308. </Space.Compact>
  1309. ))}
  1310. </Form.Item>
  1311. <Form.Item label={<Tooltip title="Physical interface for outbound traffic. Use 'auto' to detect; auto-enabled when Auto system routes is set.">Auto outbounds interface</Tooltip>}>
  1312. <Input value={ib.settings.autoOutboundsInterface} placeholder="auto"
  1313. onChange={(e) => { ib.settings.autoOutboundsInterface = e.target.value; refresh(); }} />
  1314. </Form.Item>
  1315. </Form>
  1316. )}
  1317. {ib.protocol === Protocols.WIREGUARD && (
  1318. <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
  1319. <Form.Item label={<>Secret key <SyncOutlined className="random-icon" onClick={regenInboundWg} /></>}>
  1320. <Input value={ib.settings.secretKey}
  1321. onChange={(e) => { ib.settings.secretKey = e.target.value; refresh(); }} />
  1322. </Form.Item>
  1323. <Form.Item label="Public key">
  1324. <Input value={ib.settings.pubKey} disabled />
  1325. </Form.Item>
  1326. <Form.Item label="MTU">
  1327. <InputNumber value={ib.settings.mtu}
  1328. onChange={(v) => { ib.settings.mtu = Number(v) || 0; refresh(); }} />
  1329. </Form.Item>
  1330. <Form.Item label="No-kernel TUN">
  1331. <Switch checked={!!ib.settings.noKernelTun}
  1332. onChange={(v) => { ib.settings.noKernelTun = v; refresh(); }} />
  1333. </Form.Item>
  1334. <Form.Item label="Peers">
  1335. <Button size="small" onClick={() => { ib.settings.addPeer(); refresh(); }}>
  1336. <PlusOutlined /> Add peer
  1337. </Button>
  1338. </Form.Item>
  1339. {(ib.settings.peers || []).map((peer: WireguardPeer, idx: number) => (
  1340. <div key={idx} className="wg-peer">
  1341. <Divider style={{ margin: '8px 0' }}>
  1342. Peer {idx + 1}
  1343. {ib.settings.peers.length > 1 && (
  1344. <DeleteOutlined className="danger-icon" onClick={() => { ib.settings.delPeer(idx); refresh(); }} />
  1345. )}
  1346. </Divider>
  1347. <Form.Item label={<>Secret key <SyncOutlined className="random-icon" onClick={() => regenWgKeypair(peer)} /></>}>
  1348. <Input value={peer.privateKey} onChange={(e) => { peer.privateKey = e.target.value; refresh(); }} />
  1349. </Form.Item>
  1350. <Form.Item label="Public key">
  1351. <Input value={peer.publicKey} onChange={(e) => { peer.publicKey = e.target.value; refresh(); }} />
  1352. </Form.Item>
  1353. <Form.Item label="PSK">
  1354. <Input value={peer.psk} onChange={(e) => { peer.psk = e.target.value; refresh(); }} />
  1355. </Form.Item>
  1356. <Form.Item label="Allowed IPs">
  1357. <Button size="small" onClick={() => { peer.allowedIPs.push(''); refresh(); }}>
  1358. <PlusOutlined />
  1359. </Button>
  1360. {(peer.allowedIPs || []).map((_ip: string, j: number) => (
  1361. <Space.Compact key={j} block className="mt-4">
  1362. <Input
  1363. value={peer.allowedIPs[j]}
  1364. onChange={(e) => { peer.allowedIPs[j] = e.target.value; refresh(); }} />
  1365. {peer.allowedIPs.length > 1 && (
  1366. <Button size="small" onClick={() => { peer.allowedIPs.splice(j, 1); refresh(); }}>
  1367. <MinusOutlined />
  1368. </Button>
  1369. )}
  1370. </Space.Compact>
  1371. ))}
  1372. </Form.Item>
  1373. <Form.Item label="Keep-alive">
  1374. <InputNumber value={peer.keepAlive} min={0}
  1375. onChange={(v) => { peer.keepAlive = Number(v) || 0; refresh(); }} />
  1376. </Form.Item>
  1377. </div>
  1378. ))}
  1379. </Form>
  1380. )}
  1381. </>
  1382. );
  1383. const renderStreamTab = () => {
  1384. const network = ib.stream?.network;
  1385. return (
  1386. <>
  1387. <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
  1388. {ib.protocol !== Protocols.HYSTERIA && (
  1389. <Form.Item label="Transmission">
  1390. <Select value={network} style={{ width: '75%' }} onChange={onNetworkChange}>
  1391. <Select.Option value="tcp">TCP (RAW)</Select.Option>
  1392. <Select.Option value="kcp">mKCP</Select.Option>
  1393. <Select.Option value="ws">WebSocket</Select.Option>
  1394. <Select.Option value="grpc">gRPC</Select.Option>
  1395. <Select.Option value="httpupgrade">HTTPUpgrade</Select.Option>
  1396. <Select.Option value="xhttp">XHTTP</Select.Option>
  1397. </Select>
  1398. </Form.Item>
  1399. )}
  1400. {network === 'tcp' && (
  1401. <>
  1402. {canEnableTls && (
  1403. <Form.Item label="Proxy Protocol">
  1404. <Switch checked={!!ib.stream.tcp.acceptProxyProtocol}
  1405. onChange={(v) => { ib.stream.tcp.acceptProxyProtocol = v; refresh(); }} />
  1406. </Form.Item>
  1407. )}
  1408. <Form.Item label={`HTTP ${t('camouflage')}`}>
  1409. <Switch checked={ib.stream.tcp.type === 'http'}
  1410. onChange={(v) => { ib.stream.tcp.type = v ? 'http' : 'none'; refresh(); }} />
  1411. </Form.Item>
  1412. {ib.stream.tcp.type === 'http' && (
  1413. <>
  1414. <Divider style={{ margin: 0 }}>{t('pages.inbounds.stream.general.request')}</Divider>
  1415. <Form.Item label={t('pages.inbounds.stream.tcp.version')}>
  1416. <Input value={ib.stream.tcp.request.version}
  1417. onChange={(e) => { ib.stream.tcp.request.version = e.target.value; refresh(); }} />
  1418. </Form.Item>
  1419. <Form.Item label={t('pages.inbounds.stream.tcp.method')}>
  1420. <Input value={ib.stream.tcp.request.method}
  1421. onChange={(e) => { ib.stream.tcp.request.method = e.target.value; refresh(); }} />
  1422. </Form.Item>
  1423. <Form.Item label={<>{t('pages.inbounds.stream.tcp.path')} <Button size="small" style={{ marginLeft: 6 }} onClick={() => { ib.stream.tcp.request.addPath('/'); refresh(); }}><PlusOutlined /></Button></>}>
  1424. {(ib.stream.tcp.request.path || []).map((_p: string, idx: number) => (
  1425. <Space.Compact key={`tcp-path-${idx}`} block className="mb-4">
  1426. <Input
  1427. value={ib.stream.tcp.request.path[idx]}
  1428. onChange={(e) => { ib.stream.tcp.request.path[idx] = e.target.value; refresh(); }} />
  1429. {ib.stream.tcp.request.path.length > 1 && (
  1430. <Button size="small" onClick={() => { ib.stream.tcp.request.removePath(idx); refresh(); }}>
  1431. <MinusOutlined />
  1432. </Button>
  1433. )}
  1434. </Space.Compact>
  1435. ))}
  1436. </Form.Item>
  1437. <Form.Item label={t('pages.inbounds.stream.tcp.requestHeader')}>
  1438. <Button size="small" onClick={() => { ib.stream.tcp.request.addHeader('Host', ''); refresh(); }}>
  1439. <PlusOutlined />
  1440. </Button>
  1441. </Form.Item>
  1442. {(ib.stream.tcp.request.headers || []).length > 0 && (
  1443. <Form.Item wrapperCol={{ span: 24 }}>
  1444. {(ib.stream.tcp.request.headers as { name: string; value: string }[]).map((h, idx) => (
  1445. <Space.Compact key={`tcp-rh-${idx}`} className="mb-8" block>
  1446. <InputAddon>{String(idx + 1)}</InputAddon>
  1447. <Input value={h.name}
  1448. placeholder={t('pages.inbounds.stream.general.name')}
  1449. onChange={(e) => { h.name = e.target.value; refresh(); }} />
  1450. <Input value={h.value}
  1451. placeholder={t('pages.inbounds.stream.general.value')}
  1452. onChange={(e) => { h.value = e.target.value; refresh(); }} />
  1453. <Button onClick={() => { ib.stream.tcp.request.removeHeader(idx); refresh(); }}>
  1454. <MinusOutlined />
  1455. </Button>
  1456. </Space.Compact>
  1457. ))}
  1458. </Form.Item>
  1459. )}
  1460. <Divider style={{ margin: 0 }}>{t('pages.inbounds.stream.general.response')}</Divider>
  1461. <Form.Item label={t('pages.inbounds.stream.tcp.version')}>
  1462. <Input value={ib.stream.tcp.response.version}
  1463. onChange={(e) => { ib.stream.tcp.response.version = e.target.value; refresh(); }} />
  1464. </Form.Item>
  1465. <Form.Item label={t('pages.inbounds.stream.tcp.status')}>
  1466. <Input value={ib.stream.tcp.response.status}
  1467. onChange={(e) => { ib.stream.tcp.response.status = e.target.value; refresh(); }} />
  1468. </Form.Item>
  1469. <Form.Item label={t('pages.inbounds.stream.tcp.statusDescription')}>
  1470. <Input value={ib.stream.tcp.response.reason}
  1471. onChange={(e) => { ib.stream.tcp.response.reason = e.target.value; refresh(); }} />
  1472. </Form.Item>
  1473. <Form.Item label={t('pages.inbounds.stream.tcp.responseHeader')}>
  1474. <Button size="small" onClick={() => { ib.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream'); refresh(); }}>
  1475. <PlusOutlined />
  1476. </Button>
  1477. </Form.Item>
  1478. {(ib.stream.tcp.response.headers || []).length > 0 && (
  1479. <Form.Item wrapperCol={{ span: 24 }}>
  1480. {(ib.stream.tcp.response.headers as { name: string; value: string }[]).map((h, idx) => (
  1481. <Space.Compact key={`tcp-rsh-${idx}`} className="mb-8" block>
  1482. <InputAddon>{String(idx + 1)}</InputAddon>
  1483. <Input value={h.name}
  1484. placeholder={t('pages.inbounds.stream.general.name')}
  1485. onChange={(e) => { h.name = e.target.value; refresh(); }} />
  1486. <Input value={h.value}
  1487. placeholder={t('pages.inbounds.stream.general.value')}
  1488. onChange={(e) => { h.value = e.target.value; refresh(); }} />
  1489. <Button onClick={() => { ib.stream.tcp.response.removeHeader(idx); refresh(); }}>
  1490. <MinusOutlined />
  1491. </Button>
  1492. </Space.Compact>
  1493. ))}
  1494. </Form.Item>
  1495. )}
  1496. </>
  1497. )}
  1498. </>
  1499. )}
  1500. {network === 'kcp' && (
  1501. <>
  1502. <Form.Item label="MTU"><InputNumber value={ib.stream.kcp.mtu} min={576} max={1460} onChange={(v) => { ib.stream.kcp.mtu = Number(v) || 0; refresh(); }} /></Form.Item>
  1503. <Form.Item label="TTI (ms)"><InputNumber value={ib.stream.kcp.tti} min={10} max={100} onChange={(v) => { ib.stream.kcp.tti = Number(v) || 0; refresh(); }} /></Form.Item>
  1504. <Form.Item label="Uplink (MB/s)"><InputNumber value={ib.stream.kcp.upCap} min={0} onChange={(v) => { ib.stream.kcp.upCap = Number(v) || 0; refresh(); }} /></Form.Item>
  1505. <Form.Item label="Downlink (MB/s)"><InputNumber value={ib.stream.kcp.downCap} min={0} onChange={(v) => { ib.stream.kcp.downCap = Number(v) || 0; refresh(); }} /></Form.Item>
  1506. <Form.Item label="CWND Multiplier"><InputNumber value={ib.stream.kcp.cwndMultiplier} min={1} onChange={(v) => { ib.stream.kcp.cwndMultiplier = Number(v) || 0; refresh(); }} /></Form.Item>
  1507. <Form.Item label="Max Sending Window"><InputNumber value={ib.stream.kcp.maxSendingWindow} min={0} onChange={(v) => { ib.stream.kcp.maxSendingWindow = Number(v) || 0; refresh(); }} /></Form.Item>
  1508. </>
  1509. )}
  1510. {network === 'ws' && (
  1511. <>
  1512. <Form.Item label="Proxy Protocol"><Switch checked={!!ib.stream.ws.acceptProxyProtocol} onChange={(v) => { ib.stream.ws.acceptProxyProtocol = v; refresh(); }} /></Form.Item>
  1513. <Form.Item label={t('host')}><Input value={ib.stream.ws.host} onChange={(e) => { ib.stream.ws.host = e.target.value; refresh(); }} /></Form.Item>
  1514. <Form.Item label={t('path')}><Input value={ib.stream.ws.path} onChange={(e) => { ib.stream.ws.path = e.target.value; refresh(); }} /></Form.Item>
  1515. <Form.Item label="Heartbeat Period"><InputNumber value={ib.stream.ws.heartbeatPeriod} min={0} onChange={(v) => { ib.stream.ws.heartbeatPeriod = Number(v) || 0; refresh(); }} /></Form.Item>
  1516. <Form.Item label={t('pages.inbounds.stream.tcp.requestHeader')}>
  1517. <Button size="small" onClick={() => { ib.stream.ws.addHeader('', ''); refresh(); }}><PlusOutlined /></Button>
  1518. </Form.Item>
  1519. {(ib.stream.ws.headers || []).length > 0 && (
  1520. <Form.Item wrapperCol={{ span: 24 }}>
  1521. {(ib.stream.ws.headers as { name: string; value: string }[]).map((h, idx) => (
  1522. <Space.Compact key={`ws-h-${idx}`} className="mb-8" block>
  1523. <InputAddon>{String(idx + 1)}</InputAddon>
  1524. <Input value={h.name}
  1525. placeholder={t('pages.inbounds.stream.general.name')}
  1526. onChange={(e) => { h.name = e.target.value; refresh(); }} />
  1527. <Input value={h.value}
  1528. placeholder={t('pages.inbounds.stream.general.value')}
  1529. onChange={(e) => { h.value = e.target.value; refresh(); }} />
  1530. <Button onClick={() => { ib.stream.ws.removeHeader(idx); refresh(); }}>
  1531. <MinusOutlined />
  1532. </Button>
  1533. </Space.Compact>
  1534. ))}
  1535. </Form.Item>
  1536. )}
  1537. </>
  1538. )}
  1539. {network === 'grpc' && (
  1540. <>
  1541. <Form.Item label="Service Name"><Input value={ib.stream.grpc.serviceName} onChange={(e) => { ib.stream.grpc.serviceName = e.target.value; refresh(); }} /></Form.Item>
  1542. <Form.Item label="Authority"><Input value={ib.stream.grpc.authority} onChange={(e) => { ib.stream.grpc.authority = e.target.value; refresh(); }} /></Form.Item>
  1543. <Form.Item label="Multi Mode"><Switch checked={!!ib.stream.grpc.multiMode} onChange={(v) => { ib.stream.grpc.multiMode = v; refresh(); }} /></Form.Item>
  1544. </>
  1545. )}
  1546. {network === 'httpupgrade' && (
  1547. <>
  1548. <Form.Item label="Proxy Protocol"><Switch checked={!!ib.stream.httpupgrade.acceptProxyProtocol} onChange={(v) => { ib.stream.httpupgrade.acceptProxyProtocol = v; refresh(); }} /></Form.Item>
  1549. <Form.Item label={t('host')}><Input value={ib.stream.httpupgrade.host} onChange={(e) => { ib.stream.httpupgrade.host = e.target.value; refresh(); }} /></Form.Item>
  1550. <Form.Item label={t('path')}><Input value={ib.stream.httpupgrade.path} onChange={(e) => { ib.stream.httpupgrade.path = e.target.value; refresh(); }} /></Form.Item>
  1551. <Form.Item label={t('pages.inbounds.stream.tcp.requestHeader')}>
  1552. <Button size="small" onClick={() => { ib.stream.httpupgrade.addHeader('', ''); refresh(); }}><PlusOutlined /></Button>
  1553. </Form.Item>
  1554. {(ib.stream.httpupgrade.headers || []).length > 0 && (
  1555. <Form.Item wrapperCol={{ span: 24 }}>
  1556. {(ib.stream.httpupgrade.headers as { name: string; value: string }[]).map((h, idx) => (
  1557. <Space.Compact key={`hu-h-${idx}`} className="mb-8" block>
  1558. <InputAddon>{String(idx + 1)}</InputAddon>
  1559. <Input value={h.name}
  1560. placeholder={t('pages.inbounds.stream.general.name')}
  1561. onChange={(e) => { h.name = e.target.value; refresh(); }} />
  1562. <Input value={h.value}
  1563. placeholder={t('pages.inbounds.stream.general.value')}
  1564. onChange={(e) => { h.value = e.target.value; refresh(); }} />
  1565. <Button onClick={() => { ib.stream.httpupgrade.removeHeader(idx); refresh(); }}>
  1566. <MinusOutlined />
  1567. </Button>
  1568. </Space.Compact>
  1569. ))}
  1570. </Form.Item>
  1571. )}
  1572. </>
  1573. )}
  1574. {network === 'xhttp' && (
  1575. <>
  1576. <Form.Item label={t('host')}><Input value={ib.stream.xhttp.host} onChange={(e) => { ib.stream.xhttp.host = e.target.value; refresh(); }} /></Form.Item>
  1577. <Form.Item label={t('path')}><Input value={ib.stream.xhttp.path} onChange={(e) => { ib.stream.xhttp.path = e.target.value; refresh(); }} /></Form.Item>
  1578. <Form.Item label={t('pages.inbounds.stream.tcp.requestHeader')}>
  1579. <Button size="small" onClick={() => { ib.stream.xhttp.addHeader('', ''); refresh(); }}><PlusOutlined /></Button>
  1580. </Form.Item>
  1581. {(ib.stream.xhttp.headers || []).length > 0 && (
  1582. <Form.Item wrapperCol={{ span: 24 }}>
  1583. {(ib.stream.xhttp.headers as { name: string; value: string }[]).map((h, idx) => (
  1584. <Space.Compact key={`xh-h-${idx}`} className="mb-8" block>
  1585. <InputAddon>{String(idx + 1)}</InputAddon>
  1586. <Input value={h.name}
  1587. placeholder={t('pages.inbounds.stream.general.name')}
  1588. onChange={(e) => { h.name = e.target.value; refresh(); }} />
  1589. <Input value={h.value}
  1590. placeholder={t('pages.inbounds.stream.general.value')}
  1591. onChange={(e) => { h.value = e.target.value; refresh(); }} />
  1592. <Button onClick={() => { ib.stream.xhttp.removeHeader(idx); refresh(); }}>
  1593. <MinusOutlined />
  1594. </Button>
  1595. </Space.Compact>
  1596. ))}
  1597. </Form.Item>
  1598. )}
  1599. <Form.Item label="Mode">
  1600. <Select value={ib.stream.xhttp.mode} style={{ width: '50%' }} onChange={(v) => { ib.stream.xhttp.mode = v; refresh(); }}>
  1601. {MODE_OPTIONS.map((m) => <Select.Option key={m} value={m}>{m}</Select.Option>)}
  1602. </Select>
  1603. </Form.Item>
  1604. {ib.stream.xhttp.mode === 'packet-up' && (
  1605. <>
  1606. <Form.Item label="Max Buffered Upload"><InputNumber value={ib.stream.xhttp.scMaxBufferedPosts} onChange={(v) => { ib.stream.xhttp.scMaxBufferedPosts = Number(v) || 0; refresh(); }} /></Form.Item>
  1607. <Form.Item label="Max Upload Size (Byte)"><Input value={ib.stream.xhttp.scMaxEachPostBytes} onChange={(e) => { ib.stream.xhttp.scMaxEachPostBytes = e.target.value; refresh(); }} /></Form.Item>
  1608. </>
  1609. )}
  1610. {ib.stream.xhttp.mode === 'stream-up' && (
  1611. <Form.Item label="Stream-Up Server"><Input value={ib.stream.xhttp.scStreamUpServerSecs} onChange={(e) => { ib.stream.xhttp.scStreamUpServerSecs = e.target.value; refresh(); }} /></Form.Item>
  1612. )}
  1613. <Form.Item label="Server Max Header Bytes"><InputNumber value={ib.stream.xhttp.serverMaxHeaderBytes} min={0} placeholder="0 (default)" onChange={(v) => { ib.stream.xhttp.serverMaxHeaderBytes = Number(v) || 0; refresh(); }} /></Form.Item>
  1614. <Form.Item label="Padding Bytes"><Input value={ib.stream.xhttp.xPaddingBytes} onChange={(e) => { ib.stream.xhttp.xPaddingBytes = e.target.value; refresh(); }} /></Form.Item>
  1615. <Form.Item label="Uplink HTTP Method">
  1616. <Select value={ib.stream.xhttp.uplinkHTTPMethod || ''} onChange={(v) => { ib.stream.xhttp.uplinkHTTPMethod = v; refresh(); }}>
  1617. <Select.Option value="">Default (POST)</Select.Option>
  1618. <Select.Option value="POST">POST</Select.Option>
  1619. <Select.Option value="PUT">PUT</Select.Option>
  1620. <Select.Option value="GET" disabled={ib.stream.xhttp.mode !== 'packet-up'}>GET (packet-up only)</Select.Option>
  1621. </Select>
  1622. </Form.Item>
  1623. <Form.Item label="Padding Obfs Mode"><Switch checked={!!ib.stream.xhttp.xPaddingObfsMode} onChange={(v) => { ib.stream.xhttp.xPaddingObfsMode = v; refresh(); }} /></Form.Item>
  1624. {ib.stream.xhttp.xPaddingObfsMode && (
  1625. <>
  1626. <Form.Item label="Padding Key"><Input value={ib.stream.xhttp.xPaddingKey} placeholder="x_padding" onChange={(e) => { ib.stream.xhttp.xPaddingKey = e.target.value; refresh(); }} /></Form.Item>
  1627. <Form.Item label="Padding Header"><Input value={ib.stream.xhttp.xPaddingHeader} placeholder="X-Padding" onChange={(e) => { ib.stream.xhttp.xPaddingHeader = e.target.value; refresh(); }} /></Form.Item>
  1628. <Form.Item label="Padding Placement">
  1629. <Select value={ib.stream.xhttp.xPaddingPlacement} onChange={(v) => { ib.stream.xhttp.xPaddingPlacement = v; refresh(); }}>
  1630. <Select.Option value="">Default (queryInHeader)</Select.Option>
  1631. <Select.Option value="queryInHeader">queryInHeader</Select.Option>
  1632. <Select.Option value="header">header</Select.Option>
  1633. <Select.Option value="cookie">cookie</Select.Option>
  1634. <Select.Option value="query">query</Select.Option>
  1635. </Select>
  1636. </Form.Item>
  1637. <Form.Item label="Padding Method">
  1638. <Select value={ib.stream.xhttp.xPaddingMethod} onChange={(v) => { ib.stream.xhttp.xPaddingMethod = v; refresh(); }}>
  1639. <Select.Option value="">Default (repeat-x)</Select.Option>
  1640. <Select.Option value="repeat-x">repeat-x</Select.Option>
  1641. <Select.Option value="tokenish">tokenish</Select.Option>
  1642. </Select>
  1643. </Form.Item>
  1644. </>
  1645. )}
  1646. <Form.Item label="Session Placement">
  1647. <Select value={ib.stream.xhttp.sessionPlacement} onChange={(v) => { ib.stream.xhttp.sessionPlacement = v; refresh(); }}>
  1648. <Select.Option value="">Default (path)</Select.Option>
  1649. <Select.Option value="path">path</Select.Option>
  1650. <Select.Option value="header">header</Select.Option>
  1651. <Select.Option value="cookie">cookie</Select.Option>
  1652. <Select.Option value="query">query</Select.Option>
  1653. </Select>
  1654. </Form.Item>
  1655. {ib.stream.xhttp.sessionPlacement && ib.stream.xhttp.sessionPlacement !== 'path' && (
  1656. <Form.Item label="Session Key"><Input value={ib.stream.xhttp.sessionKey} placeholder="x_session" onChange={(e) => { ib.stream.xhttp.sessionKey = e.target.value; refresh(); }} /></Form.Item>
  1657. )}
  1658. <Form.Item label="Sequence Placement">
  1659. <Select value={ib.stream.xhttp.seqPlacement} onChange={(v) => { ib.stream.xhttp.seqPlacement = v; refresh(); }}>
  1660. <Select.Option value="">Default (path)</Select.Option>
  1661. <Select.Option value="path">path</Select.Option>
  1662. <Select.Option value="header">header</Select.Option>
  1663. <Select.Option value="cookie">cookie</Select.Option>
  1664. <Select.Option value="query">query</Select.Option>
  1665. </Select>
  1666. </Form.Item>
  1667. {ib.stream.xhttp.seqPlacement && ib.stream.xhttp.seqPlacement !== 'path' && (
  1668. <Form.Item label="Sequence Key"><Input value={ib.stream.xhttp.seqKey} placeholder="x_seq" onChange={(e) => { ib.stream.xhttp.seqKey = e.target.value; refresh(); }} /></Form.Item>
  1669. )}
  1670. {ib.stream.xhttp.mode === 'packet-up' && (
  1671. <Form.Item label="Uplink Data Placement">
  1672. <Select value={ib.stream.xhttp.uplinkDataPlacement} onChange={(v) => { ib.stream.xhttp.uplinkDataPlacement = v; refresh(); }}>
  1673. <Select.Option value="">Default (body)</Select.Option>
  1674. <Select.Option value="body">body</Select.Option>
  1675. <Select.Option value="header">header</Select.Option>
  1676. <Select.Option value="cookie">cookie</Select.Option>
  1677. <Select.Option value="query">query</Select.Option>
  1678. </Select>
  1679. </Form.Item>
  1680. )}
  1681. {ib.stream.xhttp.mode === 'packet-up' && ib.stream.xhttp.uplinkDataPlacement && ib.stream.xhttp.uplinkDataPlacement !== 'body' && (
  1682. <Form.Item label="Uplink Data Key"><Input value={ib.stream.xhttp.uplinkDataKey} placeholder="x_data" onChange={(e) => { ib.stream.xhttp.uplinkDataKey = e.target.value; refresh(); }} /></Form.Item>
  1683. )}
  1684. <Form.Item label="No SSE Header"><Switch checked={!!ib.stream.xhttp.noSSEHeader} onChange={(v) => { ib.stream.xhttp.noSSEHeader = v; refresh(); }} /></Form.Item>
  1685. </>
  1686. )}
  1687. <Form.Item label="External Proxy">
  1688. <Switch checked={externalProxyOn} onChange={setExternalProxy} />
  1689. {externalProxyOn && (
  1690. <Button size="small" type="primary" style={{ marginLeft: 10 }}
  1691. onClick={() => { ib.stream.externalProxy.push({ forceTls: 'same', dest: '', port: 443, remark: '', sni: '', fingerprint: '', alpn: [] }); refresh(); }}>
  1692. <PlusOutlined />
  1693. </Button>
  1694. )}
  1695. </Form.Item>
  1696. {externalProxyOn && (
  1697. <Form.Item wrapperCol={{ span: 24 }}>
  1698. {(ib.stream.externalProxy as { forceTls: string; dest: string; port: number; remark: string; sni?: string; fingerprint?: string; alpn?: string[] }[]).map((row, idx) => (
  1699. <div key={`ep-${idx}`} style={{ margin: '8px 0' }}>
  1700. <Space.Compact block>
  1701. <Tooltip title="Force TLS">
  1702. <Select value={row.forceTls} style={{ width: '20%' }} onChange={(v) => { row.forceTls = v; refresh(); }}>
  1703. <Select.Option value="same">{t('pages.inbounds.same')}</Select.Option>
  1704. <Select.Option value="none">{t('none')}</Select.Option>
  1705. <Select.Option value="tls">TLS</Select.Option>
  1706. </Select>
  1707. </Tooltip>
  1708. <Input style={{ width: '30%' }} value={row.dest} placeholder={t('host')}
  1709. onChange={(e) => { row.dest = e.target.value; refresh(); }} />
  1710. <Tooltip title={t('pages.inbounds.port')}>
  1711. <InputNumber value={row.port} style={{ width: '15%' }} min={1} max={65535}
  1712. onChange={(v) => { row.port = Number(v) || 0; refresh(); }} />
  1713. </Tooltip>
  1714. <Input style={{ width: '25%' }} value={row.remark} placeholder={t('pages.inbounds.remark')}
  1715. onChange={(e) => { row.remark = e.target.value; refresh(); }} />
  1716. <InputAddon onClick={() => { ib.stream.externalProxy.splice(idx, 1); refresh(); }}>
  1717. <MinusOutlined />
  1718. </InputAddon>
  1719. </Space.Compact>
  1720. {row.forceTls === 'tls' && (
  1721. <Space.Compact style={{ marginTop: 6 }} block>
  1722. <Input style={{ width: '30%' }} value={row.sni || ''} placeholder="SNI (defaults to host)"
  1723. onChange={(e) => { row.sni = e.target.value; refresh(); }} />
  1724. <Select value={row.fingerprint || ''} style={{ width: '30%' }} placeholder="Fingerprint"
  1725. onChange={(v) => { row.fingerprint = v; refresh(); }}>
  1726. <Select.Option value="">Default</Select.Option>
  1727. {FINGERPRINTS.map((fp) => <Select.Option key={fp} value={fp}>{fp}</Select.Option>)}
  1728. </Select>
  1729. <Select mode="multiple" value={row.alpn || []} style={{ width: '40%' }} placeholder="ALPN"
  1730. onChange={(v) => { row.alpn = v; refresh(); }}>
  1731. {ALPNS.map((alpn) => <Select.Option key={alpn} value={alpn}>{alpn}</Select.Option>)}
  1732. </Select>
  1733. </Space.Compact>
  1734. )}
  1735. </div>
  1736. ))}
  1737. </Form.Item>
  1738. )}
  1739. <Form.Item label="Sockopt"><Switch checked={!!ib.stream.sockoptSwitch} onChange={(v) => { ib.stream.sockoptSwitch = v; refresh(); }} /></Form.Item>
  1740. {ib.stream.sockoptSwitch && ib.stream.sockopt && (
  1741. <>
  1742. <Form.Item label="Route Mark"><InputNumber value={ib.stream.sockopt.mark} min={0} onChange={(v) => { ib.stream.sockopt.mark = Number(v) || 0; refresh(); }} /></Form.Item>
  1743. <Form.Item label="TCP Keep Alive Interval"><InputNumber value={ib.stream.sockopt.tcpKeepAliveInterval} min={0} onChange={(v) => { ib.stream.sockopt.tcpKeepAliveInterval = Number(v) || 0; refresh(); }} /></Form.Item>
  1744. <Form.Item label="TCP Keep Alive Idle"><InputNumber value={ib.stream.sockopt.tcpKeepAliveIdle} min={0} onChange={(v) => { ib.stream.sockopt.tcpKeepAliveIdle = Number(v) || 0; refresh(); }} /></Form.Item>
  1745. <Form.Item label="TCP Max Seg"><InputNumber value={ib.stream.sockopt.tcpMaxSeg} min={0} onChange={(v) => { ib.stream.sockopt.tcpMaxSeg = Number(v) || 0; refresh(); }} /></Form.Item>
  1746. <Form.Item label="TCP User Timeout"><InputNumber value={ib.stream.sockopt.tcpUserTimeout} min={0} onChange={(v) => { ib.stream.sockopt.tcpUserTimeout = Number(v) || 0; refresh(); }} /></Form.Item>
  1747. <Form.Item label="TCP Window Clamp"><InputNumber value={ib.stream.sockopt.tcpWindowClamp} min={0} onChange={(v) => { ib.stream.sockopt.tcpWindowClamp = Number(v) || 0; refresh(); }} /></Form.Item>
  1748. <Form.Item label="Proxy Protocol"><Switch checked={!!ib.stream.sockopt.acceptProxyProtocol} onChange={(v) => { ib.stream.sockopt.acceptProxyProtocol = v; refresh(); }} /></Form.Item>
  1749. <Form.Item label="TCP Fast Open"><Switch checked={!!ib.stream.sockopt.tcpFastOpen} onChange={(v) => { ib.stream.sockopt.tcpFastOpen = v; refresh(); }} /></Form.Item>
  1750. <Form.Item label="Multipath TCP"><Switch checked={!!ib.stream.sockopt.tcpMptcp} onChange={(v) => { ib.stream.sockopt.tcpMptcp = v; refresh(); }} /></Form.Item>
  1751. <Form.Item label="Penetrate"><Switch checked={!!ib.stream.sockopt.penetrate} onChange={(v) => { ib.stream.sockopt.penetrate = v; refresh(); }} /></Form.Item>
  1752. <Form.Item label="V6 Only"><Switch checked={!!ib.stream.sockopt.V6Only} onChange={(v) => { ib.stream.sockopt.V6Only = v; refresh(); }} /></Form.Item>
  1753. <Form.Item label="Domain Strategy">
  1754. <Select value={ib.stream.sockopt.domainStrategy} style={{ width: '50%' }} onChange={(v) => { ib.stream.sockopt.domainStrategy = v; refresh(); }}>
  1755. {DOMAIN_STRATEGIES.map((d) => <Select.Option key={d} value={d}>{d}</Select.Option>)}
  1756. </Select>
  1757. </Form.Item>
  1758. <Form.Item label="TCP Congestion">
  1759. <Select value={ib.stream.sockopt.tcpcongestion} style={{ width: '50%' }} onChange={(v) => { ib.stream.sockopt.tcpcongestion = v; refresh(); }}>
  1760. {TCP_CONGESTIONS.map((c) => <Select.Option key={c} value={c}>{c}</Select.Option>)}
  1761. </Select>
  1762. </Form.Item>
  1763. <Form.Item label="TProxy">
  1764. <Select value={ib.stream.sockopt.tproxy} style={{ width: '50%' }} onChange={(v) => { ib.stream.sockopt.tproxy = v; refresh(); }}>
  1765. <Select.Option value="off">Off</Select.Option>
  1766. <Select.Option value="redirect">Redirect</Select.Option>
  1767. <Select.Option value="tproxy">TProxy</Select.Option>
  1768. </Select>
  1769. </Form.Item>
  1770. <Form.Item label="Dialer Proxy"><Input value={ib.stream.sockopt.dialerProxy} onChange={(e) => { ib.stream.sockopt.dialerProxy = e.target.value; refresh(); }} /></Form.Item>
  1771. <Form.Item label="Interface Name"><Input value={ib.stream.sockopt.interfaceName} onChange={(e) => { ib.stream.sockopt.interfaceName = e.target.value; refresh(); }} /></Form.Item>
  1772. <Form.Item label="Trusted X-Forwarded-For">
  1773. <Select mode="tags" value={ib.stream.sockopt.trustedXForwardedFor} style={{ width: '100%' }}
  1774. tokenSeparators={[',']}
  1775. onChange={(v) => { ib.stream.sockopt.trustedXForwardedFor = v; refresh(); }}>
  1776. <Select.Option value="CF-Connecting-IP">CF-Connecting-IP</Select.Option>
  1777. <Select.Option value="X-Real-IP">X-Real-IP</Select.Option>
  1778. <Select.Option value="True-Client-IP">True-Client-IP</Select.Option>
  1779. <Select.Option value="X-Client-IP">X-Client-IP</Select.Option>
  1780. </Select>
  1781. </Form.Item>
  1782. </>
  1783. )}
  1784. {ib.protocol === Protocols.HYSTERIA && (
  1785. <>
  1786. <Form.Item label={<Tooltip title="Hysteria protocol version. Currently must be 2.">Version</Tooltip>}>
  1787. <InputNumber value={ib.stream.hysteria.version} min={2} max={2} onChange={(v) => { ib.stream.hysteria.version = Number(v) || 2; refresh(); }} />
  1788. </Form.Item>
  1789. <Form.Item label={<Tooltip title="Idle timeout (seconds) for a single QUIC native UDP connection.">UDP idle timeout</Tooltip>}>
  1790. <InputNumber value={ib.stream.hysteria.udpIdleTimeout} min={0} onChange={(v) => { ib.stream.hysteria.udpIdleTimeout = Number(v) || 0; refresh(); }} />
  1791. </Form.Item>
  1792. <Form.Item label="Masquerade">
  1793. <Switch checked={!!ib.stream.hysteria.masqueradeSwitch} onChange={(v) => { ib.stream.hysteria.masqueradeSwitch = v; refresh(); }} />
  1794. </Form.Item>
  1795. {ib.stream.hysteria.masqueradeSwitch && (
  1796. <>
  1797. <Form.Item label="Type">
  1798. <Select value={ib.stream.hysteria.masquerade.type} style={{ width: '50%' }} onChange={(v) => { ib.stream.hysteria.masquerade.type = v; refresh(); }}>
  1799. <Select.Option value="proxy">Proxy</Select.Option>
  1800. <Select.Option value="file">File</Select.Option>
  1801. <Select.Option value="string">String</Select.Option>
  1802. </Select>
  1803. </Form.Item>
  1804. {ib.stream.hysteria.masquerade.type === 'proxy' && (
  1805. <>
  1806. <Form.Item label="URL"><Input value={ib.stream.hysteria.masquerade.url} placeholder="https://example.com" onChange={(e) => { ib.stream.hysteria.masquerade.url = e.target.value; refresh(); }} /></Form.Item>
  1807. <Form.Item label="Rewrite Host"><Switch checked={!!ib.stream.hysteria.masquerade.rewriteHost} onChange={(v) => { ib.stream.hysteria.masquerade.rewriteHost = v; refresh(); }} /></Form.Item>
  1808. <Form.Item label="Insecure"><Switch checked={!!ib.stream.hysteria.masquerade.insecure} onChange={(v) => { ib.stream.hysteria.masquerade.insecure = v; refresh(); }} /></Form.Item>
  1809. </>
  1810. )}
  1811. {ib.stream.hysteria.masquerade.type === 'file' && (
  1812. <Form.Item label="Directory"><Input value={ib.stream.hysteria.masquerade.dir} placeholder="/path/to/www" onChange={(e) => { ib.stream.hysteria.masquerade.dir = e.target.value; refresh(); }} /></Form.Item>
  1813. )}
  1814. {ib.stream.hysteria.masquerade.type === 'string' && (
  1815. <>
  1816. <Form.Item label="Content"><TextArea value={ib.stream.hysteria.masquerade.content} autoSize={{ minRows: 2, maxRows: 6 }} onChange={(e) => { ib.stream.hysteria.masquerade.content = e.target.value; refresh(); }} /></Form.Item>
  1817. <Form.Item label="Status Code"><InputNumber value={ib.stream.hysteria.masquerade.statusCode} min={100} max={599} placeholder="200" onChange={(v) => { ib.stream.hysteria.masquerade.statusCode = Number(v) || 0; refresh(); }} /></Form.Item>
  1818. <Form.Item label="Headers">
  1819. <Button size="small" onClick={() => { ib.stream.hysteria.masquerade.addHeader('', ''); refresh(); }}>
  1820. <PlusOutlined />
  1821. </Button>
  1822. </Form.Item>
  1823. {(ib.stream.hysteria.masquerade.headers || []).length > 0 && (
  1824. <Form.Item wrapperCol={{ span: 24 }}>
  1825. {(ib.stream.hysteria.masquerade.headers as { name: string; value: string }[]).map((h, idx) => (
  1826. <Space.Compact key={`mh-${idx}`} className="mb-8" block>
  1827. <InputAddon>{String(idx + 1)}</InputAddon>
  1828. <Input value={h.name} placeholder="Name"
  1829. onChange={(e) => { h.name = e.target.value; refresh(); }} />
  1830. <Input value={h.value} placeholder="Value"
  1831. onChange={(e) => { h.value = e.target.value; refresh(); }} />
  1832. <Button onClick={() => { ib.stream.hysteria.masquerade.removeHeader(idx); refresh(); }}>
  1833. <MinusOutlined />
  1834. </Button>
  1835. </Space.Compact>
  1836. ))}
  1837. </Form.Item>
  1838. )}
  1839. </>
  1840. )}
  1841. </>
  1842. )}
  1843. </>
  1844. )}
  1845. </Form>
  1846. <FinalMaskForm stream={ib.stream} protocol={ib.protocol} onChange={refresh} />
  1847. </>
  1848. );
  1849. };
  1850. const renderSecurityTab = () => (
  1851. <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
  1852. <Form.Item label={t('pages.inbounds.securityTab')}>
  1853. <Radio.Group value={ib.stream.security} buttonStyle="solid" disabled={!canEnableTls}
  1854. onChange={(e) => setSecurity(e.target.value)}>
  1855. <Radio.Button value="none">none</Radio.Button>
  1856. <Radio.Button value="tls">tls</Radio.Button>
  1857. {canEnableReality && <Radio.Button value="reality">reality</Radio.Button>}
  1858. </Radio.Group>
  1859. </Form.Item>
  1860. {ib.stream.security === 'tls' && ib.stream.tls && (
  1861. <>
  1862. <Form.Item label="SNI"><Input value={ib.stream.tls.sni} placeholder="Server Name Indication" onChange={(e) => { ib.stream.tls.sni = e.target.value; refresh(); }} /></Form.Item>
  1863. <Form.Item label="Cipher Suites">
  1864. <Select value={ib.stream.tls.cipherSuites} onChange={(v) => { ib.stream.tls.cipherSuites = v; refresh(); }}>
  1865. <Select.Option value="">Auto</Select.Option>
  1866. {CIPHER_SUITES.map(([label, val]) => <Select.Option key={val} value={val}>{label}</Select.Option>)}
  1867. </Select>
  1868. </Form.Item>
  1869. <Form.Item label="Min/Max Version">
  1870. <Space.Compact block>
  1871. <Select value={ib.stream.tls.minVersion} style={{ width: '50%' }} onChange={(v) => { ib.stream.tls.minVersion = v; refresh(); }}>
  1872. {TLS_VERSIONS.map((v) => <Select.Option key={v} value={v}>{v}</Select.Option>)}
  1873. </Select>
  1874. <Select value={ib.stream.tls.maxVersion} style={{ width: '50%' }} onChange={(v) => { ib.stream.tls.maxVersion = v; refresh(); }}>
  1875. {TLS_VERSIONS.map((v) => <Select.Option key={v} value={v}>{v}</Select.Option>)}
  1876. </Select>
  1877. </Space.Compact>
  1878. </Form.Item>
  1879. <Form.Item label="uTLS">
  1880. <Select value={ib.stream.tls.settings.fingerprint} style={{ width: '100%' }} onChange={(v) => { ib.stream.tls.settings.fingerprint = v; refresh(); }}>
  1881. <Select.Option value="">None</Select.Option>
  1882. {FINGERPRINTS.map((fp) => <Select.Option key={fp} value={fp}>{fp}</Select.Option>)}
  1883. </Select>
  1884. </Form.Item>
  1885. <Form.Item label="ALPN">
  1886. <Select mode="multiple" value={ib.stream.tls.alpn} style={{ width: '100%' }} tokenSeparators={[',']}
  1887. onChange={(v) => { ib.stream.tls.alpn = v; refresh(); }}>
  1888. {ALPNS.map((a) => <Select.Option key={a} value={a}>{a}</Select.Option>)}
  1889. </Select>
  1890. </Form.Item>
  1891. <Form.Item label="Reject Unknown SNI"><Switch checked={!!ib.stream.tls.rejectUnknownSni} onChange={(v) => { ib.stream.tls.rejectUnknownSni = v; refresh(); }} /></Form.Item>
  1892. <Form.Item label="Disable System Root"><Switch checked={!!ib.stream.tls.disableSystemRoot} onChange={(v) => { ib.stream.tls.disableSystemRoot = v; refresh(); }} /></Form.Item>
  1893. <Form.Item label="Session Resumption"><Switch checked={!!ib.stream.tls.enableSessionResumption} onChange={(v) => { ib.stream.tls.enableSessionResumption = v; refresh(); }} /></Form.Item>
  1894. {(ib.stream.tls.certs || []).map((cert: TlsCert, idx: number) => (
  1895. <div key={`cert-${idx}`}>
  1896. <Form.Item label={t('certificate')}>
  1897. <Radio.Group value={cert.useFile} buttonStyle="solid" onChange={(e) => { cert.useFile = e.target.value; refresh(); }}>
  1898. <Radio.Button value={true}>{t('pages.inbounds.certificatePath')}</Radio.Button>
  1899. <Radio.Button value={false}>{t('pages.inbounds.certificateContent')}</Radio.Button>
  1900. </Radio.Group>
  1901. </Form.Item>
  1902. <Form.Item label=" ">
  1903. <Space>
  1904. {idx === 0 && (
  1905. <Button type="primary" size="small" onClick={() => { ib.stream.tls.addCert(); refresh(); }}>
  1906. <PlusOutlined />
  1907. </Button>
  1908. )}
  1909. {ib.stream.tls.certs.length > 1 && (
  1910. <Button type="primary" size="small" onClick={() => { ib.stream.tls.removeCert(idx); refresh(); }}>
  1911. <MinusOutlined />
  1912. </Button>
  1913. )}
  1914. </Space>
  1915. </Form.Item>
  1916. {cert.useFile ? (
  1917. <>
  1918. <Form.Item label={t('pages.inbounds.publicKey')}>
  1919. <Input value={cert.certFile} onChange={(e) => { cert.certFile = e.target.value; refresh(); }} />
  1920. </Form.Item>
  1921. <Form.Item label={t('pages.inbounds.privatekey')}>
  1922. <Input value={cert.keyFile} onChange={(e) => { cert.keyFile = e.target.value; refresh(); }} />
  1923. </Form.Item>
  1924. <Form.Item label=" ">
  1925. <Button type="primary" disabled={!defaultCert && !defaultKey} onClick={() => setDefaultCertData(idx)}>
  1926. {t('pages.inbounds.setDefaultCert')}
  1927. </Button>
  1928. </Form.Item>
  1929. </>
  1930. ) : (
  1931. <>
  1932. <Form.Item label={t('pages.inbounds.publicKey')}>
  1933. <TextArea value={cert.cert} autoSize={{ minRows: 3, maxRows: 8 }}
  1934. onChange={(e) => { cert.cert = e.target.value; refresh(); }} />
  1935. </Form.Item>
  1936. <Form.Item label={t('pages.inbounds.privatekey')}>
  1937. <TextArea value={cert.key} autoSize={{ minRows: 3, maxRows: 8 }}
  1938. onChange={(e) => { cert.key = e.target.value; refresh(); }} />
  1939. </Form.Item>
  1940. </>
  1941. )}
  1942. <Form.Item label="One Time Loading"><Switch checked={!!cert.oneTimeLoading} onChange={(v) => { cert.oneTimeLoading = v; refresh(); }} /></Form.Item>
  1943. <Form.Item label="Usage Option">
  1944. <Select value={cert.usage} style={{ width: '50%' }} onChange={(v) => { cert.usage = v; refresh(); }}>
  1945. {USAGES.map((u) => <Select.Option key={u} value={u}>{u}</Select.Option>)}
  1946. </Select>
  1947. </Form.Item>
  1948. {cert.usage === 'issue' && (
  1949. <Form.Item label="Build Chain"><Switch checked={!!cert.buildChain} onChange={(v) => { cert.buildChain = v; refresh(); }} /></Form.Item>
  1950. )}
  1951. </div>
  1952. ))}
  1953. <Form.Item label="ECH key"><Input value={ib.stream.tls.echServerKeys} onChange={(e) => { ib.stream.tls.echServerKeys = e.target.value; refresh(); }} /></Form.Item>
  1954. <Form.Item label="ECH config"><Input value={ib.stream.tls.settings.echConfigList} onChange={(e) => { ib.stream.tls.settings.echConfigList = e.target.value; refresh(); }} /></Form.Item>
  1955. <Form.Item label=" ">
  1956. <Space>
  1957. <Button type="primary" loading={saving} onClick={getNewEchCert}>Get New ECH Cert</Button>
  1958. <Button danger onClick={clearEchCert}>Clear</Button>
  1959. </Space>
  1960. </Form.Item>
  1961. </>
  1962. )}
  1963. {ib.stream.security === 'reality' && ib.stream.reality && (
  1964. <>
  1965. <Form.Item label="Show"><Switch checked={!!ib.stream.reality.show} onChange={(v) => { ib.stream.reality.show = v; refresh(); }} /></Form.Item>
  1966. <Form.Item label="Xver"><InputNumber value={ib.stream.reality.xver} min={0} onChange={(v) => { ib.stream.reality.xver = Number(v) || 0; refresh(); }} /></Form.Item>
  1967. <Form.Item label="uTLS">
  1968. <Select value={ib.stream.reality.settings.fingerprint} style={{ width: '100%' }} onChange={(v) => { ib.stream.reality.settings.fingerprint = v; refresh(); }}>
  1969. {FINGERPRINTS.map((fp) => <Select.Option key={fp} value={fp}>{fp}</Select.Option>)}
  1970. </Select>
  1971. </Form.Item>
  1972. <Form.Item label={<>Target <SyncOutlined className="random-icon" onClick={randomizeRealityTarget} /></>}>
  1973. <Input value={ib.stream.reality.target} onChange={(e) => { ib.stream.reality.target = e.target.value; refresh(); }} />
  1974. </Form.Item>
  1975. <Form.Item label={<>SNI <SyncOutlined className="random-icon" onClick={randomizeRealityTarget} /></>}>
  1976. <Input value={ib.stream.reality.serverNames} onChange={(e) => { ib.stream.reality.serverNames = e.target.value; refresh(); }} />
  1977. </Form.Item>
  1978. <Form.Item label="Max Time Diff (ms)"><InputNumber value={ib.stream.reality.maxTimediff} min={0} onChange={(v) => { ib.stream.reality.maxTimediff = Number(v) || 0; refresh(); }} /></Form.Item>
  1979. <Form.Item label="Min Client Ver"><Input value={ib.stream.reality.minClientVer} placeholder="25.9.11" onChange={(e) => { ib.stream.reality.minClientVer = e.target.value; refresh(); }} /></Form.Item>
  1980. <Form.Item label="Max Client Ver"><Input value={ib.stream.reality.maxClientVer} placeholder="25.9.11" onChange={(e) => { ib.stream.reality.maxClientVer = e.target.value; refresh(); }} /></Form.Item>
  1981. <Form.Item label={<>Short IDs <SyncOutlined className="random-icon" onClick={randomizeShortIds} /></>}>
  1982. <TextArea value={ib.stream.reality.shortIds} autoSize={{ minRows: 1, maxRows: 4 }} onChange={(e) => { ib.stream.reality.shortIds = e.target.value; refresh(); }} />
  1983. </Form.Item>
  1984. <Form.Item label="SpiderX"><Input value={ib.stream.reality.settings.spiderX} onChange={(e) => { ib.stream.reality.settings.spiderX = e.target.value; refresh(); }} /></Form.Item>
  1985. <Form.Item label={t('pages.inbounds.publicKey')}>
  1986. <TextArea value={ib.stream.reality.settings.publicKey} autoSize={{ minRows: 1, maxRows: 4 }}
  1987. onChange={(e) => { ib.stream.reality.settings.publicKey = e.target.value; refresh(); }} />
  1988. </Form.Item>
  1989. <Form.Item label={t('pages.inbounds.privatekey')}>
  1990. <TextArea value={ib.stream.reality.privateKey} autoSize={{ minRows: 1, maxRows: 4 }}
  1991. onChange={(e) => { ib.stream.reality.privateKey = e.target.value; refresh(); }} />
  1992. </Form.Item>
  1993. <Form.Item label=" ">
  1994. <Space>
  1995. <Button type="primary" loading={saving} onClick={genRealityKeypair}>Get New Cert</Button>
  1996. <Button danger onClick={clearRealityKeypair}>Clear</Button>
  1997. </Space>
  1998. </Form.Item>
  1999. <Form.Item label="mldsa65 Seed">
  2000. <TextArea value={ib.stream.reality.mldsa65Seed} autoSize={{ minRows: 2, maxRows: 6 }} onChange={(e) => { ib.stream.reality.mldsa65Seed = e.target.value; refresh(); }} />
  2001. </Form.Item>
  2002. <Form.Item label="mldsa65 Verify">
  2003. <TextArea value={ib.stream.reality.settings.mldsa65Verify} autoSize={{ minRows: 2, maxRows: 6 }} onChange={(e) => { ib.stream.reality.settings.mldsa65Verify = e.target.value; refresh(); }} />
  2004. </Form.Item>
  2005. <Form.Item label=" ">
  2006. <Space>
  2007. <Button type="primary" loading={saving} onClick={genMldsa65}>Get New Seed</Button>
  2008. <Button danger onClick={clearMldsa65}>Clear</Button>
  2009. </Space>
  2010. </Form.Item>
  2011. </>
  2012. )}
  2013. </Form>
  2014. );
  2015. const renderSniffingTab = () => (
  2016. <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
  2017. <Form.Item label={t('enable')}>
  2018. <Switch checked={!!ib.sniffing.enabled} onChange={(v) => { ib.sniffing.enabled = v; refresh(); }} />
  2019. </Form.Item>
  2020. {ib.sniffing.enabled && (
  2021. <>
  2022. <Form.Item wrapperCol={{ span: 24 }}>
  2023. <Checkbox.Group value={ib.sniffing.destOverride} onChange={(v) => { ib.sniffing.destOverride = v; refresh(); }}>
  2024. {Object.entries(SNIFFING_OPTION).map(([key, value]) => (
  2025. <Checkbox key={key} value={value}>{key}</Checkbox>
  2026. ))}
  2027. </Checkbox.Group>
  2028. </Form.Item>
  2029. <Form.Item label={t('pages.inbounds.sniffingMetadataOnly')}>
  2030. <Switch checked={!!ib.sniffing.metadataOnly} onChange={(v) => { ib.sniffing.metadataOnly = v; refresh(); }} />
  2031. </Form.Item>
  2032. <Form.Item label={t('pages.inbounds.sniffingRouteOnly')}>
  2033. <Switch checked={!!ib.sniffing.routeOnly} onChange={(v) => { ib.sniffing.routeOnly = v; refresh(); }} />
  2034. </Form.Item>
  2035. <Form.Item label={t('pages.inbounds.sniffingIpsExcluded')}>
  2036. <Select mode="tags" value={ib.sniffing.ipsExcluded} tokenSeparators={[',']}
  2037. placeholder="IP/CIDR/geoip:*/ext:*" style={{ width: '100%' }}
  2038. onChange={(v) => { ib.sniffing.ipsExcluded = v; refresh(); }} />
  2039. </Form.Item>
  2040. <Form.Item label={t('pages.inbounds.sniffingDomainsExcluded')}>
  2041. <Select mode="tags" value={ib.sniffing.domainsExcluded} tokenSeparators={[',']}
  2042. placeholder="domain:*/ext:*" style={{ width: '100%' }}
  2043. onChange={(v) => { ib.sniffing.domainsExcluded = v; refresh(); }} />
  2044. </Form.Item>
  2045. </>
  2046. )}
  2047. </Form>
  2048. );
  2049. const renderAdvancedTab = () => {
  2050. const advancedTabItems = [
  2051. {
  2052. key: 'all',
  2053. label: t('pages.inbounds.advanced.all'),
  2054. children: (
  2055. <>
  2056. <div className="advanced-editor-meta">{t('pages.inbounds.advanced.allHelp')}</div>
  2057. <JsonEditor value={advancedAllValue} onChange={setAdvancedAllValue} minHeight="340px" maxHeight="560px" />
  2058. </>
  2059. ),
  2060. },
  2061. {
  2062. key: 'settings',
  2063. label: t('pages.inbounds.advanced.settings'),
  2064. children: (
  2065. <>
  2066. <div className="advanced-editor-meta">
  2067. {t('pages.inbounds.advanced.settingsHelp')} <code>{'{ settings: { ... } }'}</code>.
  2068. </div>
  2069. <JsonEditor value={wrappedConfigValue('settings', 'settings')}
  2070. onChange={(v) => setWrappedConfigValue('settings', 'settings', 'Settings', v)}
  2071. minHeight="320px" maxHeight="540px" />
  2072. </>
  2073. ),
  2074. },
  2075. {
  2076. key: 'sniffingSection',
  2077. label: t('pages.inbounds.advanced.sniffing'),
  2078. children: (
  2079. <>
  2080. <div className="advanced-editor-meta">
  2081. {t('pages.inbounds.advanced.sniffingHelp')} <code>{'{ sniffing: { ... } }'}</code>.
  2082. </div>
  2083. <JsonEditor value={wrappedConfigValue('sniffing', 'sniffing')}
  2084. onChange={(v) => setWrappedConfigValue('sniffing', 'sniffing', 'Sniffing', v)}
  2085. minHeight="240px" maxHeight="420px" />
  2086. </>
  2087. ),
  2088. },
  2089. ];
  2090. if (canEnableStream) {
  2091. advancedTabItems.push({
  2092. key: 'streamSection',
  2093. label: t('pages.inbounds.advanced.stream'),
  2094. children: (
  2095. <>
  2096. <div className="advanced-editor-meta">
  2097. {t('pages.inbounds.advanced.streamHelp')} <code>{'{ streamSettings: { ... } }'}</code>.
  2098. </div>
  2099. <JsonEditor value={wrappedConfigValue('streamSettings', 'stream')}
  2100. onChange={(v) => setWrappedConfigValue('streamSettings', 'stream', 'Stream', v)}
  2101. minHeight="320px" maxHeight="540px" />
  2102. </>
  2103. ),
  2104. });
  2105. }
  2106. return (
  2107. <div className="advanced-shell">
  2108. <div className="advanced-panel">
  2109. <div className="advanced-panel__header">
  2110. <div>
  2111. <div className="advanced-panel__title">{t('pages.inbounds.advanced.title')}</div>
  2112. <div className="advanced-panel__subtitle">{t('pages.inbounds.advanced.subtitle')}</div>
  2113. </div>
  2114. </div>
  2115. <Tabs activeKey={advancedSectionKey} onChange={setAdvancedSectionKey} items={advancedTabItems} className="advanced-inner-tabs" />
  2116. </div>
  2117. </div>
  2118. );
  2119. };
  2120. const tabItems = [
  2121. { key: 'basic', label: t('pages.xray.basicTemplate'), children: renderBasicsTab() },
  2122. ];
  2123. if (hasProtocolTabContent) {
  2124. tabItems.push({ key: 'protocol', label: t('pages.inbounds.protocol'), children: renderProtocolTab() });
  2125. }
  2126. if (canEnableStream) {
  2127. tabItems.push({ key: 'stream', label: t('pages.inbounds.streamTab'), children: renderStreamTab() });
  2128. tabItems.push({ key: 'security', label: t('pages.inbounds.securityTab'), children: renderSecurityTab() });
  2129. }
  2130. tabItems.push({ key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: renderSniffingTab() });
  2131. tabItems.push({ key: 'advanced', label: t('pages.xray.advancedTemplate'), children: renderAdvancedTab() });
  2132. return (
  2133. <>
  2134. {messageContextHolder}
  2135. <Modal
  2136. open={open}
  2137. title={title}
  2138. okText={okText}
  2139. cancelText={t('close')}
  2140. confirmLoading={saving}
  2141. mask={{ closable: false }}
  2142. width={780}
  2143. onOk={submit}
  2144. onCancel={onClose}
  2145. destroyOnHidden
  2146. >
  2147. <Tabs activeKey={activeTabKey} onChange={handleTabChange} items={tabItems} />
  2148. </Modal>
  2149. </>
  2150. );
  2151. }