useSecurityActions.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. import type { Dispatch, SetStateAction } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import type { UseFormReturn } from 'react-hook-form';
  4. import type { MessageInstance } from 'antd/es/message/interface';
  5. import { HttpUtil, RandomUtil } from '@/utils';
  6. import { createTlsSettingsWithDefaultCert } from '@/lib/xray/inbound-tls-defaults';
  7. import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
  8. import type { InboundFormValues } from '@/schemas/forms/inbound-form';
  9. import type { RealityScanResult } from '@/generated/types';
  10. interface UseSecurityActionsArgs {
  11. methods: UseFormReturn<InboundFormValues>;
  12. setSaving: Dispatch<SetStateAction<boolean>>;
  13. messageApi: MessageInstance;
  14. /*
  15. * Node the inbound is deployed to (null = central panel). "Set Cert from
  16. * Panel" must read the node's own cert paths for a node-assigned inbound —
  17. * the central panel's paths don't exist on the node. See issue #4854.
  18. */
  19. nodeId: number | null;
  20. setScanResult: Dispatch<SetStateAction<RealityScanResult | null>>;
  21. setScanning: Dispatch<SetStateAction<boolean>>;
  22. }
  23. /*
  24. * Server-side TLS / Reality key + certificate generation handlers for the
  25. * inbound modal's security tab. Each talks to a /panel server endpoint and
  26. * writes the result back into the form. Lifted out of InboundFormModal so
  27. * the modal body stays focused on orchestration.
  28. */
  29. export function useSecurityActions({ methods, setSaving, messageApi, nodeId, setScanResult, setScanning }: UseSecurityActionsArgs) {
  30. const { t } = useTranslation();
  31. const setValue = methods.setValue as unknown as (name: string, value: unknown) => void;
  32. const getValues = methods.getValues as unknown as (name?: string) => unknown;
  33. const genRealityKeypair = async () => {
  34. setSaving(true);
  35. try {
  36. const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
  37. if (msg?.success) {
  38. const obj = msg.obj as { privateKey: string; publicKey: string };
  39. setValue('streamSettings.realitySettings.privateKey', obj.privateKey);
  40. setValue('streamSettings.realitySettings.settings.publicKey', obj.publicKey);
  41. }
  42. } finally {
  43. setSaving(false);
  44. }
  45. };
  46. const clearRealityKeypair = () => {
  47. setValue('streamSettings.realitySettings.privateKey', '');
  48. setValue('streamSettings.realitySettings.settings.publicKey', '');
  49. };
  50. const genMldsa65 = async () => {
  51. setSaving(true);
  52. try {
  53. const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
  54. if (msg?.success) {
  55. const obj = msg.obj as { seed: string; verify: string };
  56. setValue('streamSettings.realitySettings.mldsa65Seed', obj.seed);
  57. setValue('streamSettings.realitySettings.settings.mldsa65Verify', obj.verify);
  58. }
  59. } finally {
  60. setSaving(false);
  61. }
  62. };
  63. const clearMldsa65 = () => {
  64. setValue('streamSettings.realitySettings.mldsa65Seed', '');
  65. setValue('streamSettings.realitySettings.settings.mldsa65Verify', '');
  66. };
  67. const applyRealityScanResult = (r: RealityScanResult) => {
  68. setScanResult(r);
  69. setValue('streamSettings.realitySettings.target', r.target);
  70. if (r.serverNames?.length) {
  71. setValue('streamSettings.realitySettings.serverNames', r.serverNames);
  72. }
  73. };
  74. const scanRealityTarget = async () => {
  75. const target = ((getValues('streamSettings.realitySettings.target') as string | undefined) ?? '').trim();
  76. if (!target) {
  77. messageApi.warning(t('pages.inbounds.form.realityTargetRequired'));
  78. return;
  79. }
  80. setScanning(true);
  81. try {
  82. const msg = await HttpUtil.post<RealityScanResult>(
  83. '/panel/api/server/scanRealityTarget',
  84. { target },
  85. { silent: true },
  86. );
  87. if (!msg?.success || !msg.obj) {
  88. setScanResult(null);
  89. messageApi.error(msg?.msg || t('pages.inbounds.toasts.scanRealityTargetError'));
  90. return;
  91. }
  92. const r = msg.obj;
  93. applyRealityScanResult(r);
  94. if (r.feasible) {
  95. messageApi.success(t('pages.inbounds.toasts.scanRealityTargetFeasible'));
  96. } else {
  97. messageApi.warning(r.reason || t('pages.inbounds.toasts.scanRealityTargetNotFeasible'));
  98. }
  99. } finally {
  100. setScanning(false);
  101. }
  102. };
  103. const scanRealityCandidates = async (targets?: string): Promise<RealityScanResult[]> => {
  104. const msg = await HttpUtil.post<RealityScanResult[]>(
  105. '/panel/api/server/scanRealityTargets',
  106. targets ? { targets } : {},
  107. { silent: true },
  108. );
  109. if (!msg?.success || !Array.isArray(msg.obj)) {
  110. messageApi.error(msg?.msg || t('pages.inbounds.toasts.scanRealityTargetError'));
  111. return [];
  112. }
  113. return msg.obj;
  114. };
  115. const randomizeShortIds = () => {
  116. setValue(
  117. 'streamSettings.realitySettings.shortIds',
  118. RandomUtil.randomShortIds().split(',').map((s) => s.trim()).filter(Boolean),
  119. );
  120. };
  121. const randomizeSpiderX = () => {
  122. setValue(
  123. 'streamSettings.realitySettings.settings.spiderX',
  124. `/${RandomUtil.randomSeq(15)}`,
  125. );
  126. };
  127. const getNewEchCert = async () => {
  128. const sni = getValues('streamSettings.tlsSettings.serverName');
  129. setSaving(true);
  130. try {
  131. const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni });
  132. if (msg?.success) {
  133. const obj = msg.obj as { echServerKeys: string; echConfigList: string };
  134. setValue('streamSettings.tlsSettings.echServerKeys', obj.echServerKeys);
  135. setValue('streamSettings.tlsSettings.settings.echConfigList', obj.echConfigList);
  136. }
  137. } finally {
  138. setSaving(false);
  139. }
  140. };
  141. const clearEchCert = () => {
  142. setValue('streamSettings.tlsSettings.echServerKeys', '');
  143. setValue('streamSettings.tlsSettings.settings.echConfigList', '');
  144. };
  145. /*
  146. * Fill the pinned-cert field from the inbound's own certificate: read the
  147. * first configured cert (file path or inline content) and ask the server for
  148. * its hex SHA-256, then merge the hash(es) into pinnedPeerCertSha256.
  149. */
  150. const pinFromCert = async () => {
  151. const certs = (getValues('streamSettings.tlsSettings.certificates') ?? []) as Array<{
  152. certificateFile?: string;
  153. certificate?: string[];
  154. }>;
  155. const first = certs[0];
  156. const certFile = first?.certificateFile?.trim() ?? '';
  157. const certContent = Array.isArray(first?.certificate) ? first.certificate.join('\n').trim() : '';
  158. if (!certFile && !certContent) {
  159. messageApi.warning(t('pages.inbounds.setDefaultCertEmpty'));
  160. return;
  161. }
  162. setSaving(true);
  163. try {
  164. const msg = await HttpUtil.post('/panel/api/server/getCertHash', { certFile, certContent });
  165. if (!msg?.success) {
  166. messageApi.warning(msg?.msg || t('pages.inbounds.setDefaultCertEmpty'));
  167. return;
  168. }
  169. const hashes = (msg.obj as string[] | undefined) ?? [];
  170. if (hashes.length === 0) return;
  171. const current = (getValues(
  172. 'streamSettings.tlsSettings.settings.pinnedPeerCertSha256',
  173. ) as string[] | undefined) ?? [];
  174. const merged = Array.from(new Set([...current, ...hashes]));
  175. setValue('streamSettings.tlsSettings.settings.pinnedPeerCertSha256', merged);
  176. } finally {
  177. setSaving(false);
  178. }
  179. };
  180. /*
  181. * Fill the pinned-cert field by pinging the configured SNI: fetches the live
  182. * remote certificate hash via `xray tls ping`. Useful when the panel doesn't
  183. * hold the cert file (a CDN front / external endpoint).
  184. */
  185. const pinFromRemote = async () => {
  186. const server = ((getValues('streamSettings.tlsSettings.serverName') as string | undefined) ?? '').trim();
  187. if (!server) {
  188. messageApi.warning(t('pages.inbounds.form.pinFromRemoteNoSni'));
  189. return;
  190. }
  191. /*
  192. * `xray tls ping` defaults to :443, but a self-hosted inbound rarely
  193. * listens there. Append the inbound's own port (unless the SNI already
  194. * carries one) so the ping reaches the actual TLS endpoint.
  195. */
  196. const port = getValues('port') as number | undefined;
  197. const target = /:\d+$/.test(server) || !port ? server : `${server}:${port}`;
  198. setSaving(true);
  199. try {
  200. const msg = await HttpUtil.post('/panel/api/server/getRemoteCertHash', { server: target });
  201. if (!msg?.success) {
  202. messageApi.warning(msg?.msg || t('pages.inbounds.form.pinFromRemoteFailed'));
  203. return;
  204. }
  205. const hashes = (msg.obj as string[] | undefined) ?? [];
  206. if (hashes.length === 0) return;
  207. const current = (getValues(
  208. 'streamSettings.tlsSettings.settings.pinnedPeerCertSha256',
  209. ) as string[] | undefined) ?? [];
  210. const merged = Array.from(new Set([...current, ...hashes]));
  211. setValue('streamSettings.tlsSettings.settings.pinnedPeerCertSha256', merged);
  212. } finally {
  213. setSaving(false);
  214. }
  215. };
  216. const setCertFromPanel = async (certName: number) => {
  217. setSaving(true);
  218. try {
  219. /*
  220. * Node-assigned inbounds run on the node, so their cert files must be the
  221. * node's own paths (fetched through the central panel), not this panel's.
  222. */
  223. const msg = typeof nodeId === 'number'
  224. ? await HttpUtil.get(`/panel/api/nodes/webCert/${nodeId}`, undefined, { silent: true })
  225. : await HttpUtil.post('/panel/api/setting/all', undefined, { silent: true });
  226. if (!msg?.success) {
  227. messageApi.warning(msg?.msg || t('pages.inbounds.setDefaultCertEmpty'));
  228. return;
  229. }
  230. const obj = msg.obj as { webCertFile?: string; webKeyFile?: string };
  231. if (!obj?.webCertFile && !obj?.webKeyFile) {
  232. messageApi.warning(t('pages.inbounds.setDefaultCertEmpty'));
  233. return;
  234. }
  235. setValue(
  236. `streamSettings.tlsSettings.certificates.${certName}.certificateFile`,
  237. obj.webCertFile ?? '',
  238. );
  239. setValue(
  240. `streamSettings.tlsSettings.certificates.${certName}.keyFile`,
  241. obj.webKeyFile ?? '',
  242. );
  243. } finally {
  244. setSaving(false);
  245. }
  246. };
  247. const clearCertFiles = (certName: number) => {
  248. setValue(
  249. `streamSettings.tlsSettings.certificates.${certName}.certificateFile`,
  250. '',
  251. );
  252. setValue(
  253. `streamSettings.tlsSettings.certificates.${certName}.keyFile`,
  254. '',
  255. );
  256. };
  257. const onSecurityChange = async (next: string) => {
  258. setScanResult(null);
  259. const current = (getValues('streamSettings') as Record<string, unknown>) ?? {};
  260. const cleaned: Record<string, unknown> = { ...current, security: next };
  261. delete cleaned.tlsSettings;
  262. delete cleaned.realitySettings;
  263. if (next === 'tls') {
  264. cleaned.tlsSettings = createTlsSettingsWithDefaultCert();
  265. }
  266. if (next === 'reality') {
  267. const reality = RealityStreamSettingsSchema.parse({}) as Record<string, unknown>;
  268. reality.target = '';
  269. reality.serverNames = [];
  270. reality.shortIds = RandomUtil.randomShortIds().split(',').map((s) => s.trim()).filter(Boolean);
  271. cleaned.realitySettings = reality;
  272. }
  273. setValue('streamSettings', cleaned);
  274. if (next === 'reality') {
  275. randomizeSpiderX();
  276. try {
  277. const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
  278. if (msg?.success) {
  279. const obj = msg.obj as { privateKey: string; publicKey: string };
  280. setValue('streamSettings.realitySettings.privateKey', obj.privateKey);
  281. setValue('streamSettings.realitySettings.settings.publicKey', obj.publicKey);
  282. }
  283. } catch {
  284. /* best-effort: leave keypair fields empty if server call fails */
  285. }
  286. }
  287. };
  288. return {
  289. genRealityKeypair,
  290. clearRealityKeypair,
  291. genMldsa65,
  292. clearMldsa65,
  293. scanRealityTarget,
  294. scanRealityCandidates,
  295. applyRealityScanResult,
  296. randomizeShortIds,
  297. randomizeSpiderX,
  298. getNewEchCert,
  299. clearEchCert,
  300. pinFromCert,
  301. pinFromRemote,
  302. setCertFromPanel,
  303. clearCertFiles,
  304. onSecurityChange,
  305. };
  306. }