InboundInfoModal.tsx 33 KB

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