InboundInfoModal.tsx 40 KB

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