InboundInfoModal.tsx 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898
  1. import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Button, Divider, Modal, Space, Tabs, Tag, Tooltip } from 'antd';
  4. import { CopyOutlined, SyncOutlined, DeleteOutlined, DownloadOutlined } from '@ant-design/icons';
  5. import { HttpUtil, IntlUtil, SizeFormatter, ColorUtils } from '@/utils';
  6. import { Protocols } from '@/schemas/primitives';
  7. import { InfinityIcon } from '@/components/ui';
  8. import { useDatepicker } from '@/hooks/useDatepicker';
  9. import {
  10. genAllLinks,
  11. genWireguardConfigs,
  12. genWireguardLinks,
  13. preferPublicHost,
  14. } from '@/lib/xray/inbound-link';
  15. import { inboundFromDb } from '@/lib/xray/inbound-from-db';
  16. import {
  17. buildInboundInfo,
  18. copyText,
  19. downloadText,
  20. formatIpInfo,
  21. hasShareLink,
  22. statsColor,
  23. } from './helpers';
  24. import type { ClientSetting, ClientStats, InboundInfo, InboundInfoModalProps } from './types';
  25. import './InboundInfoModal.css';
  26. export default function InboundInfoModal({
  27. open,
  28. onClose,
  29. dbInbound,
  30. clientIndex = 0,
  31. expireDiff = 0,
  32. trafficDiff = 0,
  33. ipLimitEnable = false,
  34. tgBotEnable = false,
  35. nodeAddress = '',
  36. subSettings,
  37. lastOnlineMap = {},
  38. }: InboundInfoModalProps) {
  39. const { t } = useTranslation();
  40. const { datepicker } = useDatepicker();
  41. const [inbound, setInbound] = useState<InboundInfo | null>(null);
  42. const [clientSettings, setClientSettings] = useState<ClientSetting | null>(null);
  43. const [clientStats, setClientStats] = useState<ClientStats | null>(null);
  44. const [links, setLinks] = useState<{ remark?: string; link: string }[]>([]);
  45. const [wireguardConfigs, setWireguardConfigs] = useState<string[]>([]);
  46. const [wireguardLinks, setWireguardLinks] = useState<string[]>([]);
  47. const [subLink, setSubLink] = useState('');
  48. const [subJsonLink, setSubJsonLink] = useState('');
  49. const [refreshing, setRefreshing] = useState(false);
  50. const [clientIpsArray, setClientIpsArray] = useState<string[]>([]);
  51. const [clientIpsText, setClientIpsText] = useState('');
  52. const [activeTab, setActiveTab] = useState('client');
  53. const loadClientIps = useCallback(async () => {
  54. if (!clientStats?.email) return;
  55. setRefreshing(true);
  56. try {
  57. const msg = await HttpUtil.post(`/panel/api/clients/ips/${clientStats.email}`);
  58. if (!msg?.success) {
  59. setClientIpsText((msg?.obj as string) || 'No IP record');
  60. setClientIpsArray([]);
  61. return;
  62. }
  63. let ips: unknown = msg.obj;
  64. if (typeof ips === 'string') {
  65. try {
  66. ips = JSON.parse(ips);
  67. } catch {
  68. setClientIpsText(String(ips));
  69. setClientIpsArray([String(ips)]);
  70. return;
  71. }
  72. }
  73. if (ips && !Array.isArray(ips) && typeof ips === 'object') ips = [ips];
  74. if (Array.isArray(ips) && ips.length > 0) {
  75. const arr = (ips as unknown[]).map(formatIpInfo).filter(Boolean) as string[];
  76. setClientIpsArray(arr);
  77. setClientIpsText(arr.join(' | '));
  78. } else {
  79. setClientIpsArray([]);
  80. setClientIpsText(String(ips || t('tgbot.noIpRecord')));
  81. }
  82. } finally {
  83. setRefreshing(false);
  84. }
  85. }, [clientStats, t]);
  86. const clearClientIps = useCallback(async () => {
  87. if (!clientStats?.email) return;
  88. const msg = await HttpUtil.post(`/panel/api/clients/clearIps/${clientStats.email}`);
  89. if (msg?.success) {
  90. setClientIpsArray([]);
  91. setClientIpsText(t('tgbot.noIpRecord'));
  92. }
  93. }, [clientStats, t]);
  94. useEffect(() => {
  95. if (!open || !dbInbound) return;
  96. const info = buildInboundInfo(dbInbound);
  97. setInbound(info);
  98. setActiveTab(info.clients.length > 0 ? 'client' : 'inbound');
  99. const idx = clientIndex ?? 0;
  100. const clientSet = info.clients.length > 0 ? (info.clients[idx] || null) : null;
  101. setClientSettings(clientSet);
  102. const stats = clientSet
  103. ? (dbInbound.clientStats || []).find((s) => s.email === clientSet.email) || null
  104. : null;
  105. setClientStats(stats);
  106. const inboundForLinks = inboundFromDb(dbInbound);
  107. const fallbackHostname = preferPublicHost(window.location.hostname, subSettings?.publicHost ?? '');
  108. if (info.protocol === Protocols.WIREGUARD) {
  109. setWireguardConfigs(
  110. genWireguardConfigs({
  111. inbound: inboundForLinks,
  112. remark: dbInbound.remark,
  113. hostOverride: nodeAddress,
  114. fallbackHostname,
  115. }).split('\r\n'),
  116. );
  117. setWireguardLinks(
  118. genWireguardLinks({
  119. inbound: inboundForLinks,
  120. remark: dbInbound.remark,
  121. hostOverride: nodeAddress,
  122. fallbackHostname,
  123. }).split('\r\n'),
  124. );
  125. setLinks([]);
  126. } else {
  127. setLinks(
  128. genAllLinks({
  129. inbound: inboundForLinks,
  130. remark: dbInbound.remark,
  131. client: (clientSet ?? {}) as Parameters<typeof genAllLinks>[0]['client'],
  132. hostOverride: nodeAddress,
  133. fallbackHostname,
  134. }),
  135. );
  136. setWireguardConfigs([]);
  137. setWireguardLinks([]);
  138. }
  139. if (clientSet?.subId) {
  140. setSubLink((subSettings?.subURI || '') + clientSet.subId);
  141. setSubJsonLink(
  142. subSettings?.subJsonEnable ? (subSettings?.subJsonURI || '') + clientSet.subId : '',
  143. );
  144. } else {
  145. setSubLink('');
  146. setSubJsonLink('');
  147. }
  148. setClientIpsArray([]);
  149. setClientIpsText('');
  150. if (ipLimitEnable && (clientSet?.limitIp ?? 0) > 0 && stats?.email) {
  151. void HttpUtil.post(`/panel/api/clients/ips/${stats.email}`).then((msg) => {
  152. if (!msg?.success) {
  153. setClientIpsText((msg?.obj as string) || 'No IP record');
  154. return;
  155. }
  156. let ips: unknown = msg.obj;
  157. if (typeof ips === 'string') {
  158. try {
  159. ips = JSON.parse(ips);
  160. } catch {
  161. setClientIpsText(String(ips));
  162. setClientIpsArray([String(ips)]);
  163. return;
  164. }
  165. }
  166. if (ips && !Array.isArray(ips) && typeof ips === 'object') ips = [ips];
  167. if (Array.isArray(ips) && ips.length > 0) {
  168. const arr = (ips as unknown[]).map(formatIpInfo).filter(Boolean) as string[];
  169. setClientIpsArray(arr);
  170. setClientIpsText(arr.join(' | '));
  171. } else {
  172. setClientIpsText(String(ips || t('tgbot.noIpRecord')));
  173. }
  174. });
  175. }
  176. }, [open, dbInbound, clientIndex, nodeAddress, subSettings, ipLimitEnable, t]);
  177. const isEnable = useMemo(() => {
  178. if (clientSettings) return !!clientSettings.enable;
  179. return dbInbound?.enable ?? true;
  180. }, [clientSettings, dbInbound]);
  181. const isDepleted = useMemo(() => {
  182. if (!clientStats || !clientSettings) return false;
  183. const total = clientStats.total ?? 0;
  184. const used = (clientStats.up ?? 0) + (clientStats.down ?? 0);
  185. if (total > 0 && used >= total) return true;
  186. const expiry = clientSettings.expiryTime ?? 0;
  187. if (expiry > 0 && Date.now() >= expiry) return true;
  188. return false;
  189. }, [clientStats, clientSettings]);
  190. const remainingStats = useMemo(() => {
  191. if (!clientStats || !clientSettings) return '-';
  192. const remained = clientStats.total - clientStats.up - clientStats.down;
  193. return remained > 0 ? SizeFormatter.sizeFormat(remained) : '-';
  194. }, [clientStats, clientSettings]);
  195. const formatLastOnline = useCallback(
  196. (email: string) => {
  197. const ts = lastOnlineMap[email];
  198. if (!ts) return '-';
  199. return IntlUtil.formatDate(ts, datepicker);
  200. },
  201. [lastOnlineMap, datepicker],
  202. );
  203. const networkLabel = inbound?.stream?.network || '';
  204. const securityLabel = inbound?.stream?.security || 'none';
  205. const securityColor = securityLabel === 'none' ? 'red' : 'green';
  206. const encryptionLabel = (inbound?.settings?.encryption as string) || '';
  207. const serverNameLabel = inbound?.serverName || '';
  208. const showClientTab = !!clientSettings;
  209. const showSubscriptionTab = !!(subSettings?.enable && clientSettings?.subId);
  210. if (!dbInbound || !inbound) {
  211. return (
  212. <Modal open={open} onCancel={onClose} title={t('pages.inbounds.inboundInfo')} footer={null} width={640} />
  213. );
  214. }
  215. const clientTab = (
  216. <>
  217. <table className="info-table block">
  218. <tbody>
  219. <tr>
  220. <td>{t('pages.inbounds.email')}</td>
  221. <td>
  222. {clientSettings?.email ? (
  223. <Tag color="green">{clientSettings.email}</Tag>
  224. ) : (
  225. <Tag color="red">{t('none')}</Tag>
  226. )}
  227. </td>
  228. </tr>
  229. {clientSettings?.id && (
  230. <tr><td>ID</td><td><Tag>{clientSettings.id}</Tag></td></tr>
  231. )}
  232. {dbInbound.isVMess && (
  233. <tr><td>{t('security')}</td><td><Tag>{clientSettings?.security}</Tag></td></tr>
  234. )}
  235. {inbound.isVlessTlsFlow && (
  236. <tr>
  237. <td>{t('pages.clients.flow')}</td>
  238. <td>
  239. {clientSettings?.flow ? <Tag>{clientSettings.flow}</Tag> : <Tag color="orange">{t('none')}</Tag>}
  240. </td>
  241. </tr>
  242. )}
  243. {clientSettings?.password && (
  244. <tr>
  245. <td>{t('password')}</td>
  246. <td><Tag className="info-large-tag">{clientSettings.password}</Tag></td>
  247. </tr>
  248. )}
  249. <tr>
  250. <td>{t('status')}</td>
  251. <td>
  252. {isDepleted ? (
  253. <Tag color="red">{t('depleted')}</Tag>
  254. ) : isEnable ? (
  255. <Tag color="green">{t('enabled')}</Tag>
  256. ) : (
  257. <Tag>{t('disabled')}</Tag>
  258. )}
  259. </td>
  260. </tr>
  261. {clientStats && (
  262. <tr>
  263. <td>{t('usage')}</td>
  264. <td>
  265. <Tag color="green">{SizeFormatter.sizeFormat(clientStats.up + clientStats.down)}</Tag>
  266. <Tag>
  267. ↑ {SizeFormatter.sizeFormat(clientStats.up)} /
  268. {' '}{SizeFormatter.sizeFormat(clientStats.down)} ↓
  269. </Tag>
  270. </td>
  271. </tr>
  272. )}
  273. <tr>
  274. <td>{t('pages.inbounds.createdAt')}</td>
  275. <td>
  276. {clientSettings?.created_at ? (
  277. <Tag>{IntlUtil.formatDate(clientSettings.created_at, datepicker)}</Tag>
  278. ) : <Tag>-</Tag>}
  279. </td>
  280. </tr>
  281. <tr>
  282. <td>{t('pages.inbounds.updatedAt')}</td>
  283. <td>
  284. {clientSettings?.updated_at ? (
  285. <Tag>{IntlUtil.formatDate(clientSettings.updated_at, datepicker)}</Tag>
  286. ) : <Tag>-</Tag>}
  287. </td>
  288. </tr>
  289. <tr>
  290. <td>{t('lastOnline')}</td>
  291. <td><Tag>{formatLastOnline(clientSettings?.email || '')}</Tag></td>
  292. </tr>
  293. {clientSettings?.comment && (
  294. <tr><td>{t('comment')}</td><td><Tag className="info-large-tag">{clientSettings.comment}</Tag></td></tr>
  295. )}
  296. {ipLimitEnable && (
  297. <tr><td>{t('pages.inbounds.IPLimit')}</td><td><Tag>{clientSettings?.limitIp ?? 0}</Tag></td></tr>
  298. )}
  299. {ipLimitEnable && (clientSettings?.limitIp ?? 0) > 0 && (
  300. <tr>
  301. <td>{t('pages.inbounds.IPLimitlog')}</td>
  302. <td>
  303. <div className="ip-log">
  304. {clientIpsArray.length > 0 ? (
  305. <div>
  306. {clientIpsArray.map((item, idx) => (
  307. <Tag color="blue" className="ip-log-row" key={idx}>{item}</Tag>
  308. ))}
  309. </div>
  310. ) : (
  311. <Tag>{clientIpsText || t('tgbot.noIpRecord')}</Tag>
  312. )}
  313. </div>
  314. <div className="ip-log-actions">
  315. <SyncOutlined spin={refreshing} onClick={() => loadClientIps()} />
  316. <Tooltip title={t('pages.inbounds.IPLimitlogclear')}>
  317. <DeleteOutlined onClick={() => clearClientIps()} />
  318. </Tooltip>
  319. </div>
  320. </td>
  321. </tr>
  322. )}
  323. </tbody>
  324. </table>
  325. <table className="info-table summary-table">
  326. <thead>
  327. <tr>
  328. <th>{t('remained')}</th>
  329. <th>{t('pages.inbounds.totalUsage')}</th>
  330. <th>{t('pages.inbounds.expireDate')}</th>
  331. </tr>
  332. </thead>
  333. <tbody>
  334. <tr>
  335. <td>
  336. {clientStats && (clientSettings?.totalGB ?? 0) > 0 ? (
  337. <Tag color={statsColor(clientStats, trafficDiff)}>{remainingStats}</Tag>
  338. ) : !clientSettings?.totalGB || clientSettings.totalGB <= 0 ? (
  339. <Tag color="purple"><InfinityIcon /></Tag>
  340. ) : null}
  341. </td>
  342. <td>
  343. {(clientSettings?.totalGB ?? 0) > 0 ? (
  344. <Tag color={clientStats ? statsColor(clientStats, trafficDiff) : 'default'}>
  345. {SizeFormatter.sizeFormat(clientSettings!.totalGB!)}
  346. </Tag>
  347. ) : (
  348. <Tag color="purple"><InfinityIcon /></Tag>
  349. )}
  350. </td>
  351. <td>
  352. {(clientSettings?.expiryTime ?? 0) > 0 ? (
  353. <Tag color={ColorUtils.usageColor(Date.now(), expireDiff, clientSettings!.expiryTime!)}>
  354. {IntlUtil.formatDate(clientSettings!.expiryTime!, datepicker)}
  355. </Tag>
  356. ) : (clientSettings?.expiryTime ?? 0) < 0 ? (
  357. <Tag color="green">{clientSettings!.expiryTime! / -86400000} {t('day')}</Tag>
  358. ) : (
  359. <Tag color="purple"><InfinityIcon /></Tag>
  360. )}
  361. </td>
  362. </tr>
  363. </tbody>
  364. </table>
  365. {tgBotEnable && clientSettings?.tgId && (
  366. <>
  367. <Divider>Telegram</Divider>
  368. <div className="tg-row">
  369. <Tag color="blue">{clientSettings.tgId}</Tag>
  370. <Tooltip title={t('copy')}>
  371. <Button size="small" icon={<CopyOutlined />} onClick={() => copyText(clientSettings.tgId, t)} />
  372. </Tooltip>
  373. </div>
  374. </>
  375. )}
  376. {hasShareLink(dbInbound.protocol) && links.length > 0 && (
  377. <>
  378. <Divider>{t('pages.inbounds.copyLink')}</Divider>
  379. {links.map((link, idx) => (
  380. <div key={idx} className="link-panel">
  381. <div className="link-panel-header">
  382. <Tag color="green">{link.remark || `Link ${idx + 1}`}</Tag>
  383. <Tooltip title={t('copy')}>
  384. <Button size="small" icon={<CopyOutlined />} onClick={() => copyText(link.link, t)} />
  385. </Tooltip>
  386. </div>
  387. <code className="link-panel-text">{link.link}</code>
  388. </div>
  389. ))}
  390. </>
  391. )}
  392. {showSubscriptionTab && (
  393. <>
  394. <Divider>{t('subscription.title')}</Divider>
  395. <div className="link-panel">
  396. <div className="link-panel-header">
  397. <Tag color="green">{t('subscription.title')}</Tag>
  398. <Tooltip title={t('copy')}>
  399. <Button size="small" icon={<CopyOutlined />} onClick={() => copyText(subLink, t)} />
  400. </Tooltip>
  401. </div>
  402. <a href={subLink} target="_blank" rel="noopener noreferrer" className="link-panel-anchor">{subLink}</a>
  403. </div>
  404. {subSettings?.subJsonEnable && subJsonLink && (
  405. <div className="link-panel">
  406. <div className="link-panel-header">
  407. <Tag color="green">JSON</Tag>
  408. <Tooltip title={t('copy')}>
  409. <Button size="small" icon={<CopyOutlined />} onClick={() => copyText(subJsonLink, t)} />
  410. </Tooltip>
  411. </div>
  412. <a href={subJsonLink} target="_blank" rel="noopener noreferrer" className="link-panel-anchor">{subJsonLink}</a>
  413. </div>
  414. )}
  415. </>
  416. )}
  417. </>
  418. );
  419. const inboundTab = (
  420. <>
  421. <dl className="info-list">
  422. <div className="info-row">
  423. <dt>{t('pages.inbounds.protocol')}</dt>
  424. <dd><Tag color="purple">{dbInbound.protocol}</Tag></dd>
  425. </div>
  426. <div className="info-row">
  427. <dt>{t('pages.inbounds.address')}</dt>
  428. <dd><Tag className="value-tag">{dbInbound.address}</Tag></dd>
  429. </div>
  430. <div className="info-row">
  431. <dt>{t('pages.inbounds.port')}</dt>
  432. <dd><Tag>{dbInbound.port}</Tag></dd>
  433. </div>
  434. {(dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS) && (
  435. <>
  436. <div className="info-row">
  437. <dt>{t('transmission')}</dt>
  438. <dd><Tag color="green">{networkLabel}</Tag></dd>
  439. </div>
  440. {(inbound.isTcp || inbound.isWs || inbound.isHttpupgrade || inbound.isXHTTP) && (
  441. <>
  442. <div className="info-row">
  443. <dt>{t('host')}</dt>
  444. <dd>{inbound.host ? <Tag className="value-tag">{inbound.host}</Tag> : <Tag color="orange">{t('none')}</Tag>}</dd>
  445. </div>
  446. <div className="info-row">
  447. <dt>{t('path')}</dt>
  448. <dd>{inbound.path ? <Tag className="value-tag">{inbound.path}</Tag> : <Tag color="orange">{t('none')}</Tag>}</dd>
  449. </div>
  450. </>
  451. )}
  452. {inbound.isXHTTP && (
  453. <div className="info-row">
  454. <dt>{t('pages.inbounds.info.mode')}</dt>
  455. <dd><Tag>{inbound.stream?.xhttp?.mode}</Tag></dd>
  456. </div>
  457. )}
  458. {inbound.isGrpc && (
  459. <>
  460. <div className="info-row">
  461. <dt>{t('pages.inbounds.info.grpcServiceName')}</dt>
  462. <dd><Tag className="value-tag">{inbound.serviceName}</Tag></dd>
  463. </div>
  464. <div className="info-row">
  465. <dt>{t('pages.inbounds.info.grpcMultiMode')}</dt>
  466. <dd><Tag>{String(inbound.stream?.grpc?.multiMode)}</Tag></dd>
  467. </div>
  468. </>
  469. )}
  470. </>
  471. )}
  472. {hasShareLink(dbInbound.protocol) && (
  473. <>
  474. <div className="info-row">
  475. <dt>{t('security')}</dt>
  476. <dd><Tag color={securityColor}>{securityLabel}</Tag></dd>
  477. </div>
  478. {encryptionLabel && (
  479. <div className="info-row">
  480. <dt>{t('encryption')}</dt>
  481. <dd className="value-block">
  482. <code className="value-code">{encryptionLabel}</code>
  483. <Tooltip title={t('copy')}>
  484. <Button size="small" className="value-copy" icon={<CopyOutlined />} onClick={() => copyText(encryptionLabel, t)} />
  485. </Tooltip>
  486. </dd>
  487. </div>
  488. )}
  489. {securityLabel !== 'none' && (
  490. <div className="info-row">
  491. <dt>{t('domainName')}</dt>
  492. <dd>
  493. {serverNameLabel ? (
  494. <Tag color="green" className="value-tag">{serverNameLabel}</Tag>
  495. ) : (
  496. <Tag color="orange">{t('none')}</Tag>
  497. )}
  498. </dd>
  499. </div>
  500. )}
  501. </>
  502. )}
  503. </dl>
  504. {dbInbound.isSS && inbound.settings && (
  505. <table className="info-table block">
  506. <tbody>
  507. <tr>
  508. <td>{t('encryption')}</td>
  509. <td><Tag color="green">{inbound.settings.method as string}</Tag></td>
  510. </tr>
  511. {inbound.isSS2022 && (
  512. <tr>
  513. <td>{t('password')}</td>
  514. <td><Tag className="info-large-tag">{inbound.settings.password as string}</Tag></td>
  515. </tr>
  516. )}
  517. <tr>
  518. <td>{t('pages.inbounds.network')}</td>
  519. <td><Tag color="green">{inbound.settings.network as string}</Tag></td>
  520. </tr>
  521. </tbody>
  522. </table>
  523. )}
  524. {inbound.protocol === Protocols.TUN && inbound.settings && (
  525. <dl className="info-list info-list-block">
  526. <div className="info-row">
  527. <dt>{t('pages.inbounds.info.interfaceName')}</dt>
  528. <dd><Tag color="green" className="value-tag">{inbound.settings.name as string}</Tag></dd>
  529. </div>
  530. <div className="info-row">
  531. <dt>{t('pages.inbounds.info.mtu')}</dt>
  532. <dd><Tag color="green">{inbound.settings.mtu as number}</Tag></dd>
  533. </div>
  534. {Array.isArray(inbound.settings.gateway) && (inbound.settings.gateway as string[]).length > 0 && (
  535. <div className="info-row">
  536. <dt>{t('pages.inbounds.info.gateway')}</dt>
  537. <dd>
  538. {(inbound.settings.gateway as string[]).map((ip, j) => (
  539. <Tag key={`tun-gw-${j}`} color="green" className="value-tag">{ip}</Tag>
  540. ))}
  541. </dd>
  542. </div>
  543. )}
  544. {Array.isArray(inbound.settings.dns) && (inbound.settings.dns as string[]).length > 0 && (
  545. <div className="info-row">
  546. <dt>{t('pages.inbounds.info.dns')}</dt>
  547. <dd>
  548. {(inbound.settings.dns as string[]).map((ip, j) => (
  549. <Tag key={`tun-dns-${j}`} color="green">{ip}</Tag>
  550. ))}
  551. </dd>
  552. </div>
  553. )}
  554. <div className="info-row">
  555. <dt>{t('pages.inbounds.info.outboundsInterface')}</dt>
  556. <dd><Tag color="green">{(inbound.settings.autoOutboundsInterface as string) || 'auto'}</Tag></dd>
  557. </div>
  558. {Array.isArray(inbound.settings.autoSystemRoutingTable) && (inbound.settings.autoSystemRoutingTable as string[]).length > 0 && (
  559. <div className="info-row">
  560. <dt>{t('pages.inbounds.info.autoSystemRoutes')}</dt>
  561. <dd>
  562. {(inbound.settings.autoSystemRoutingTable as string[]).map((cidr, j) => (
  563. <Tag key={`tun-rt-${j}`} color="green">{cidr}</Tag>
  564. ))}
  565. </dd>
  566. </div>
  567. )}
  568. </dl>
  569. )}
  570. {inbound.protocol === Protocols.TUNNEL && inbound.settings && (
  571. <dl className="info-list info-list-block">
  572. <div className="info-row">
  573. <dt>{t('pages.inbounds.targetAddress')}</dt>
  574. <dd><Tag color="green" className="value-tag">{inbound.settings.rewriteAddress as string}</Tag></dd>
  575. </div>
  576. <div className="info-row">
  577. <dt>{t('pages.inbounds.destinationPort')}</dt>
  578. <dd><Tag color="green">{inbound.settings.rewritePort as number}</Tag></dd>
  579. </div>
  580. <div className="info-row">
  581. <dt>{t('pages.inbounds.network')}</dt>
  582. <dd><Tag color="green">{inbound.settings.allowedNetwork as string}</Tag></dd>
  583. </div>
  584. <div className="info-row">
  585. <dt>{t('pages.inbounds.info.followRedirect')}</dt>
  586. <dd>
  587. <Tag color={inbound.settings.followRedirect ? 'green' : 'red'}>
  588. {inbound.settings.followRedirect ? t('enabled') : t('disabled')}
  589. </Tag>
  590. </dd>
  591. </div>
  592. </dl>
  593. )}
  594. {inbound.protocol === Protocols.MTPROTO && inbound.settings && (
  595. <dl className="info-list info-list-block">
  596. <div className="info-row">
  597. <dt>{t('pages.inbounds.form.fakeTlsDomain')}</dt>
  598. <dd><Tag color="green" className="value-tag">{inbound.settings.fakeTlsDomain as string}</Tag></dd>
  599. </div>
  600. <div className="info-row">
  601. <dt>{t('pages.inbounds.form.mtprotoSecret')}</dt>
  602. <dd className="value-block">
  603. <code className="value-code">{inbound.settings.secret as string}</code>
  604. <Tooltip title={t('copy')}>
  605. <Button size="small" className="value-copy" icon={<CopyOutlined />} onClick={() => copyText(inbound.settings.secret as string, t)} />
  606. </Tooltip>
  607. </dd>
  608. </div>
  609. {(() => {
  610. const s = inbound.settings;
  611. const df = s.domainFronting as { ip?: string; port?: number; proxyProtocol?: boolean } | undefined;
  612. const frontingTarget = df && (df.ip || df.port)
  613. ? `${df.ip ?? ''}${df.port ? `:${df.port}` : ''}`
  614. : '';
  615. return (
  616. <>
  617. {frontingTarget && (
  618. <div className="info-row">
  619. <dt>{t('pages.inbounds.form.mtgDomainFrontingIp')}</dt>
  620. <dd><Tag color="blue" className="value-tag">{frontingTarget}</Tag></dd>
  621. </div>
  622. )}
  623. {df?.proxyProtocol && (
  624. <div className="info-row">
  625. <dt>{t('pages.inbounds.form.mtgDomainFrontingProxyProtocol')}</dt>
  626. <dd><Tag color="green" className="value-tag">{t('enabled')}</Tag></dd>
  627. </div>
  628. )}
  629. {Boolean(s.proxyProtocolListener) && (
  630. <div className="info-row">
  631. <dt>{t('pages.inbounds.form.mtgProxyProtocolListener')}</dt>
  632. <dd><Tag color="green" className="value-tag">{t('enabled')}</Tag></dd>
  633. </div>
  634. )}
  635. {Boolean(s.preferIp) && (
  636. <div className="info-row">
  637. <dt>{t('pages.inbounds.form.mtgPreferIp')}</dt>
  638. <dd><Tag color="blue" className="value-tag">{s.preferIp as string}</Tag></dd>
  639. </div>
  640. )}
  641. {Boolean(s.debug) && (
  642. <div className="info-row">
  643. <dt>{t('pages.inbounds.form.mtgDebug')}</dt>
  644. <dd><Tag color="green" className="value-tag">{t('enabled')}</Tag></dd>
  645. </div>
  646. )}
  647. </>
  648. );
  649. })()}
  650. {links.length > 0 && (
  651. <div className="info-row">
  652. <dt>{t('pages.inbounds.copyLink')}</dt>
  653. <dd className="value-block">
  654. <code className="value-code">{links[0].link}</code>
  655. <Tooltip title={t('copy')}>
  656. <Button size="small" className="value-copy" icon={<CopyOutlined />} onClick={() => copyText(links[0].link, t)} />
  657. </Tooltip>
  658. </dd>
  659. </div>
  660. )}
  661. </dl>
  662. )}
  663. {dbInbound.isMixed && inbound.settings && (
  664. <dl className="info-list info-list-block">
  665. <div className="info-row">
  666. <dt>{t('pages.inbounds.info.auth')}</dt>
  667. <dd>
  668. <Tag color={inbound.settings.auth === 'password' ? 'green' : 'orange'}>
  669. {inbound.settings.auth as string}
  670. </Tag>
  671. </dd>
  672. </div>
  673. <div className="info-row">
  674. <dt>UDP</dt>
  675. <dd>
  676. <Tag color={inbound.settings.udp ? 'green' : 'red'}>
  677. {inbound.settings.udp ? t('enabled') : t('disabled')}
  678. </Tag>
  679. </dd>
  680. </div>
  681. {(inbound.settings.ip as string) && (
  682. <div className="info-row">
  683. <dt>IP</dt>
  684. <dd><Tag className="value-tag">{inbound.settings.ip as string}</Tag></dd>
  685. </div>
  686. )}
  687. {inbound.settings.auth === 'password' && Array.isArray(inbound.settings.accounts) && (
  688. <>
  689. {(inbound.settings.accounts as { user: string; pass: string }[]).map((account, idx) => (
  690. <div key={idx} className="info-row">
  691. <dt>{t('username')} #{idx + 1}</dt>
  692. <dd className="account-row">
  693. <Tag color="green" className="value-tag">{account.user}</Tag>
  694. <span className="account-sep">:</span>
  695. <Tag className="value-tag">{account.pass}</Tag>
  696. <Tooltip title={t('copy')}>
  697. <Button size="small" type="text" icon={<CopyOutlined />} onClick={() => copyText(`${account.user}:${account.pass}`, t)} />
  698. </Tooltip>
  699. <Space size={4} wrap className="share-buttons">
  700. <Tooltip title={`socks5://${account.user}:${account.pass}@${dbInbound.address}:${dbInbound.port}`}>
  701. <Button size="small" onClick={() => copyText(`socks5://${account.user}:${account.pass}@${dbInbound.address}:${dbInbound.port}`, t)}>SOCKS5</Button>
  702. </Tooltip>
  703. <Tooltip title={`http://${account.user}:${account.pass}@${dbInbound.address}:${dbInbound.port}`}>
  704. <Button size="small" onClick={() => copyText(`http://${account.user}:${account.pass}@${dbInbound.address}:${dbInbound.port}`, t)}>HTTP</Button>
  705. </Tooltip>
  706. <Tooltip title="https://t.me/socks?server=...&port=...&user=...&pass=...">
  707. <Button size="small" onClick={() => copyText(`https://t.me/socks?server=${encodeURIComponent(dbInbound.address)}&port=${dbInbound.port}&user=${encodeURIComponent(account.user)}&pass=${encodeURIComponent(account.pass)}`, t)}>Telegram</Button>
  708. </Tooltip>
  709. </Space>
  710. </dd>
  711. </div>
  712. ))}
  713. </>
  714. )}
  715. {inbound.settings.auth === 'noauth' && (
  716. <div className="info-row">
  717. <dt>{t('copy')}</dt>
  718. <dd>
  719. <Space size={4} wrap className="share-buttons">
  720. <Tooltip title={`socks5://${dbInbound.address}:${dbInbound.port}`}>
  721. <Button size="small" onClick={() => copyText(`socks5://${dbInbound.address}:${dbInbound.port}`, t)}>SOCKS5</Button>
  722. </Tooltip>
  723. <Tooltip title={`http://${dbInbound.address}:${dbInbound.port}`}>
  724. <Button size="small" onClick={() => copyText(`http://${dbInbound.address}:${dbInbound.port}`, t)}>HTTP</Button>
  725. </Tooltip>
  726. <Tooltip title="https://t.me/socks?server=...&port=...">
  727. <Button size="small" onClick={() => copyText(`https://t.me/socks?server=${encodeURIComponent(dbInbound.address)}&port=${dbInbound.port}`, t)}>Telegram</Button>
  728. </Tooltip>
  729. </Space>
  730. </dd>
  731. </div>
  732. )}
  733. </dl>
  734. )}
  735. {dbInbound.isHTTP && Array.isArray(inbound.settings?.accounts) && (inbound.settings!.accounts as unknown[]).length > 0 && (
  736. <dl className="info-list info-list-block">
  737. {(inbound.settings!.accounts as { user: string; pass: string }[]).map((account, idx) => (
  738. <div key={idx} className="info-row">
  739. <dt>{t('username')} #{idx + 1}</dt>
  740. <dd className="account-row">
  741. <Tag color="green" className="value-tag">{account.user}</Tag>
  742. <span className="account-sep">:</span>
  743. <Tag className="value-tag">{account.pass}</Tag>
  744. <Tooltip title={t('copy')}>
  745. <Button size="small" icon={<CopyOutlined />} onClick={() => copyText(`${account.user}:${account.pass}`, t)} />
  746. </Tooltip>
  747. </dd>
  748. </div>
  749. ))}
  750. </dl>
  751. )}
  752. {dbInbound.isWireguard && inbound.settings && (
  753. <>
  754. <dl className="info-list info-list-block">
  755. <div className="info-row">
  756. <dt>{t('pages.xray.wireguard.secretKey')}</dt>
  757. <dd><Tag className="value-tag">{inbound.settings.secretKey as string}</Tag></dd>
  758. </div>
  759. <div className="info-row">
  760. <dt>{t('pages.xray.wireguard.publicKey')}</dt>
  761. <dd><Tag className="value-tag">{inbound.settings.pubKey as string}</Tag></dd>
  762. </div>
  763. <div className="info-row">
  764. <dt>{t('pages.inbounds.info.mtu')}</dt>
  765. <dd><Tag>{inbound.settings.mtu as number}</Tag></dd>
  766. </div>
  767. <div className="info-row">
  768. <dt>{t('pages.inbounds.info.noKernelTun')}</dt>
  769. <dd>
  770. <Tag color={inbound.settings.noKernelTun ? 'green' : 'default'}>
  771. {String(inbound.settings.noKernelTun)}
  772. </Tag>
  773. </dd>
  774. </div>
  775. </dl>
  776. {Array.isArray(inbound.settings.peers) && (inbound.settings.peers as { privateKey: string; publicKey: string; psk: string; allowedIPs?: string[]; keepAlive?: number }[]).map((peer, idx) => (
  777. <Fragment key={idx}>
  778. <Divider>{t('pages.inbounds.info.peerNumber', { n: idx + 1 })}</Divider>
  779. <dl className="info-list info-list-block">
  780. <div className="info-row">
  781. <dt>{t('pages.xray.wireguard.secretKey')}</dt>
  782. <dd><Tag className="value-tag">{peer.privateKey}</Tag></dd>
  783. </div>
  784. <div className="info-row">
  785. <dt>{t('pages.xray.wireguard.publicKey')}</dt>
  786. <dd><Tag className="value-tag">{peer.publicKey}</Tag></dd>
  787. </div>
  788. <div className="info-row">
  789. <dt>PSK</dt>
  790. <dd><Tag className="value-tag">{peer.psk}</Tag></dd>
  791. </div>
  792. <div className="info-row">
  793. <dt>{t('pages.xray.wireguard.allowedIPs')}</dt>
  794. <dd>
  795. {(peer.allowedIPs || []).map((ip, j) => (
  796. <Tag key={`wg-ip-${idx}-${j}`} className="value-tag">{ip}</Tag>
  797. ))}
  798. </dd>
  799. </div>
  800. <div className="info-row">
  801. <dt>{t('pages.inbounds.info.keepAlive')}</dt>
  802. <dd><Tag>{peer.keepAlive}</Tag></dd>
  803. </div>
  804. </dl>
  805. {wireguardConfigs[idx] && (
  806. <div className="link-panel">
  807. <div className="link-panel-header">
  808. <Tag color="green">{t('pages.inbounds.info.peerNumberConfig', { n: idx + 1 })}</Tag>
  809. <Tooltip title={t('copy')}>
  810. <Button size="small" icon={<CopyOutlined />} onClick={() => copyText(wireguardConfigs[idx], t)} />
  811. </Tooltip>
  812. <Tooltip title={t('download')}>
  813. <Button size="small" icon={<DownloadOutlined />} onClick={() => downloadText(wireguardConfigs[idx], `peer-${idx + 1}.conf`)} />
  814. </Tooltip>
  815. </div>
  816. <code className="link-panel-text">{wireguardConfigs[idx]}</code>
  817. </div>
  818. )}
  819. {wireguardLinks[idx] && (
  820. <div className="link-panel">
  821. <div className="link-panel-header">
  822. <Tag color="green">Peer {idx + 1} link</Tag>
  823. <Tooltip title={t('copy')}>
  824. <Button size="small" icon={<CopyOutlined />} onClick={() => copyText(wireguardLinks[idx], t)} />
  825. </Tooltip>
  826. </div>
  827. <code className="link-panel-text">{wireguardLinks[idx]}</code>
  828. </div>
  829. )}
  830. </Fragment>
  831. ))}
  832. </>
  833. )}
  834. {dbInbound.isSS && !inbound.isSSMultiUser && links.length > 0 && (
  835. <>
  836. <Divider>{t('pages.inbounds.copyLink')}</Divider>
  837. {links.map((link, idx) => (
  838. <div key={idx} className="link-panel">
  839. <div className="link-panel-header">
  840. <Tag color="green">{link.remark || `Link ${idx + 1}`}</Tag>
  841. <Tooltip title={t('copy')}>
  842. <Button size="small" icon={<CopyOutlined />} onClick={() => copyText(link.link, t)} />
  843. </Tooltip>
  844. </div>
  845. <code className="link-panel-text">{link.link}</code>
  846. </div>
  847. ))}
  848. </>
  849. )}
  850. </>
  851. );
  852. const tabItems = [];
  853. if (showClientTab) {
  854. tabItems.push({ key: 'client', label: t('pages.inbounds.client'), children: clientTab });
  855. }
  856. tabItems.push({ key: 'inbound', label: t('pages.xray.rules.inbound'), children: inboundTab });
  857. return (
  858. <Modal open={open} onCancel={onClose} title={t('pages.inbounds.inboundInfo')} footer={null} width={640} destroyOnHidden>
  859. <Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
  860. </Modal>
  861. );
  862. }