| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- import type { Dispatch, SetStateAction } from 'react';
- import { useTranslation } from 'react-i18next';
- import type { UseFormReturn } from 'react-hook-form';
- import type { MessageInstance } from 'antd/es/message/interface';
- import type { HookAPI as ModalHookAPI } from 'antd/es/modal/useModal';
- import { HttpUtil, RandomUtil } from '@/utils';
- import { createTlsSettingsWithDefaultCert } from '@/lib/xray/inbound-tls-defaults';
- import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
- import type { InboundFormValues } from '@/schemas/forms/inbound-form';
- import type { RealityScanResult } from '@/generated/types';
- interface UseSecurityActionsArgs {
- methods: UseFormReturn<InboundFormValues>;
- setSaving: Dispatch<SetStateAction<boolean>>;
- messageApi: MessageInstance;
- modal: ModalHookAPI;
- /*
- * Node the inbound is deployed to (null = central panel). "Set Cert from
- * Panel" must read the node's own cert paths for a node-assigned inbound —
- * the central panel's paths don't exist on the node. See issue #4854.
- */
- nodeId: number | null;
- setScanResult: Dispatch<SetStateAction<RealityScanResult | null>>;
- setScanning: Dispatch<SetStateAction<boolean>>;
- }
- /*
- * Server-side TLS / Reality key + certificate generation handlers for the
- * inbound modal's security tab. Each talks to a /panel server endpoint and
- * writes the result back into the form. Lifted out of InboundFormModal so
- * the modal body stays focused on orchestration.
- */
- export function useSecurityActions({
- methods,
- setSaving,
- messageApi,
- modal,
- nodeId,
- setScanResult,
- setScanning,
- }: UseSecurityActionsArgs) {
- const { t } = useTranslation();
- const setValue = methods.setValue as unknown as (name: string, value: unknown) => void;
- const getValues = methods.getValues as unknown as (name?: string) => unknown;
- const genRealityKeypair = async () => {
- setSaving(true);
- try {
- const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
- if (msg?.success) {
- const obj = msg.obj as { privateKey: string; publicKey: string };
- setValue('streamSettings.realitySettings.privateKey', obj.privateKey);
- setValue('streamSettings.realitySettings.settings.publicKey', obj.publicKey);
- }
- } finally {
- setSaving(false);
- }
- };
- const clearRealityKeypair = () => {
- setValue('streamSettings.realitySettings.privateKey', '');
- setValue('streamSettings.realitySettings.settings.publicKey', '');
- };
- const genMldsa65 = async () => {
- setSaving(true);
- try {
- const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
- if (msg?.success) {
- const obj = msg.obj as { seed: string; verify: string };
- setValue('streamSettings.realitySettings.mldsa65Seed', obj.seed);
- setValue('streamSettings.realitySettings.settings.mldsa65Verify', obj.verify);
- }
- } finally {
- setSaving(false);
- }
- };
- const clearMldsa65 = () => {
- setValue('streamSettings.realitySettings.mldsa65Seed', '');
- setValue('streamSettings.realitySettings.settings.mldsa65Verify', '');
- };
- /*
- * replaceServerNames is for picking a target wholesale: keeping the previous
- * target's SNI would leave a REALITY config that cannot work.
- */
- const applyRealityScanResult = (r: RealityScanResult, replaceServerNames = false) => {
- setScanResult(r);
- setValue('streamSettings.realitySettings.target', r.target);
- /*
- * Names off an untrusted chain are not usable as SNI; names off a trusted
- * one are, even when the SNI sent did not match them, which is how a stale
- * SNI recovers instead of failing every rescan.
- */
- if (replaceServerNames) {
- setValue('streamSettings.realitySettings.serverNames', r.serverNames ?? []);
- } else if ((r.certValid || r.certChainValid) && r.serverNames?.length) {
- setValue('streamSettings.realitySettings.serverNames', r.serverNames);
- }
- };
- const scanRealityTarget = async (allowPrivate = false) => {
- const target = (
- (getValues('streamSettings.realitySettings.target') as string | undefined) ?? ''
- ).trim();
- if (!target) {
- messageApi.warning(t('pages.inbounds.form.realityTargetRequired'));
- return;
- }
- const xver = Number(getValues('streamSettings.realitySettings.xver')) || 0;
- /*
- * Clients dial the target but send an SNI from serverNames, so the probe
- * must too — a fronting proxy answers a bare target name with its default
- * certificate, which then reads as an untrusted target.
- */
- const serverNames =
- (getValues('streamSettings.realitySettings.serverNames') as string[] | undefined) ?? [];
- const sni = (serverNames.find((n) => typeof n === 'string' && n.trim() !== '') ?? '').trim();
- setScanning(true);
- try {
- const msg = await HttpUtil.post<RealityScanResult>(
- '/panel/api/server/scanRealityTarget',
- { target, sni, xver, allowPrivate },
- { silent: true },
- );
- if (!msg?.success || !msg.obj) {
- setScanResult(null);
- messageApi.error(msg?.msg || t('pages.inbounds.toasts.scanRealityTargetError'));
- return;
- }
- const r = msg.obj;
- applyRealityScanResult(r);
- /*
- * The SSRF guard refuses a LAN/Docker target until the operator confirms
- * it; the retry carries the opt-in for this one probe.
- */
- if (r.privateTarget && !allowPrivate) {
- modal.confirm({
- title: t('pages.inbounds.form.scanPrivateConfirmTitle'),
- content: t('pages.inbounds.form.scanPrivateConfirmContent', {
- target: r.target || target,
- }),
- okText: t('confirm'),
- cancelText: t('cancel'),
- onOk: () => scanRealityTarget(true),
- });
- return;
- }
- if (!r.feasible) {
- messageApi.warning(r.reason || t('pages.inbounds.toasts.scanRealityTargetNotFeasible'));
- } else if (r.privateTarget) {
- messageApi.warning(t('pages.inbounds.toasts.scanRealityTargetPrivate'));
- } else {
- messageApi.success(t('pages.inbounds.toasts.scanRealityTargetFeasible'));
- }
- } finally {
- setScanning(false);
- }
- };
- const scanRealityCandidates = async (targets?: string): Promise<RealityScanResult[]> => {
- const msg = await HttpUtil.post<RealityScanResult[]>(
- '/panel/api/server/scanRealityTargets',
- targets ? { targets } : {},
- { silent: true },
- );
- if (!msg?.success || !Array.isArray(msg.obj)) {
- messageApi.error(msg?.msg || t('pages.inbounds.toasts.scanRealityTargetError'));
- return [];
- }
- return msg.obj;
- };
- const randomizeShortIds = () => {
- setValue(
- 'streamSettings.realitySettings.shortIds',
- RandomUtil.randomShortIds()
- .split(',')
- .map((s) => s.trim())
- .filter(Boolean),
- );
- };
- const randomizeSpiderX = () => {
- setValue('streamSettings.realitySettings.settings.spiderX', `/${RandomUtil.randomSeq(15)}`);
- };
- const getNewEchCert = async () => {
- const sni = getValues('streamSettings.tlsSettings.serverName');
- setSaving(true);
- try {
- const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni });
- if (msg?.success) {
- const obj = msg.obj as { echServerKeys: string; echConfigList: string };
- setValue('streamSettings.tlsSettings.echServerKeys', obj.echServerKeys);
- setValue('streamSettings.tlsSettings.settings.echConfigList', obj.echConfigList);
- }
- } finally {
- setSaving(false);
- }
- };
- const clearEchCert = () => {
- setValue('streamSettings.tlsSettings.echServerKeys', '');
- setValue('streamSettings.tlsSettings.settings.echConfigList', '');
- };
- /*
- * Fill the pinned-cert field from the inbound's own certificate: read the
- * first configured cert (file path or inline content) and ask the server for
- * its hex SHA-256, then merge the hash(es) into pinnedPeerCertSha256.
- */
- const pinFromCert = async () => {
- const certs = (getValues('streamSettings.tlsSettings.certificates') ?? []) as Array<{
- certificateFile?: string;
- certificate?: string[];
- }>;
- const first = certs[0];
- const certFile = first?.certificateFile?.trim() ?? '';
- const certContent = Array.isArray(first?.certificate)
- ? first.certificate.join('\n').trim()
- : '';
- if (!certFile && !certContent) {
- messageApi.warning(t('pages.inbounds.setDefaultCertEmpty'));
- return;
- }
- setSaving(true);
- try {
- const msg = await HttpUtil.post('/panel/api/server/getCertHash', { certFile, certContent });
- if (!msg?.success) {
- messageApi.warning(msg?.msg || t('pages.inbounds.setDefaultCertEmpty'));
- return;
- }
- const hashes = (msg.obj as string[] | undefined) ?? [];
- if (hashes.length === 0) return;
- const current =
- (getValues('streamSettings.tlsSettings.settings.pinnedPeerCertSha256') as
- | string[]
- | undefined) ?? [];
- const merged = Array.from(new Set([...current, ...hashes]));
- setValue('streamSettings.tlsSettings.settings.pinnedPeerCertSha256', merged);
- } finally {
- setSaving(false);
- }
- };
- /*
- * Fill the pinned-cert field by pinging the configured SNI: fetches the live
- * remote certificate hash via `xray tls ping`. Useful when the panel doesn't
- * hold the cert file (a CDN front / external endpoint).
- */
- const pinFromRemote = async (allowPrivate = false) => {
- const server = (
- (getValues('streamSettings.tlsSettings.serverName') as string | undefined) ?? ''
- ).trim();
- if (!server) {
- messageApi.warning(t('pages.inbounds.form.pinFromRemoteNoSni'));
- return;
- }
- /*
- * `xray tls ping` defaults to :443, but a self-hosted inbound rarely
- * listens there. Append the inbound's own port (unless the SNI already
- * carries one) so the ping reaches the actual TLS endpoint.
- */
- const port = getValues('port') as number | undefined;
- const target = /:\d+$/.test(server) || !port ? server : `${server}:${port}`;
- setSaving(true);
- try {
- const msg = await HttpUtil.post(
- '/panel/api/server/getRemoteCertHash',
- { server: target, allowPrivate },
- { silent: true },
- );
- // The SSRF guard refuses a LAN/loopback endpoint until the operator confirms it.
- const blocked = (msg?.obj as { privateTarget?: boolean } | null | undefined)?.privateTarget;
- if (!msg?.success && blocked && !allowPrivate) {
- modal.confirm({
- title: t('pages.inbounds.form.scanPrivateConfirmTitle'),
- content: t('pages.inbounds.form.scanPrivateConfirmContent', { target }),
- okText: t('confirm'),
- cancelText: t('cancel'),
- onOk: () => pinFromRemote(true),
- });
- return;
- }
- if (!msg?.success) {
- messageApi.warning(msg?.msg || t('pages.inbounds.form.pinFromRemoteFailed'));
- return;
- }
- const hashes = (msg.obj as string[] | undefined) ?? [];
- if (hashes.length === 0) return;
- const current =
- (getValues('streamSettings.tlsSettings.settings.pinnedPeerCertSha256') as
- | string[]
- | undefined) ?? [];
- const merged = Array.from(new Set([...current, ...hashes]));
- setValue('streamSettings.tlsSettings.settings.pinnedPeerCertSha256', merged);
- } finally {
- setSaving(false);
- }
- };
- const setCertFromPanel = async (certName: number) => {
- setSaving(true);
- try {
- /*
- * Node-assigned inbounds run on the node, so their cert files must be the
- * node's own paths (fetched through the central panel), not this panel's.
- */
- const msg =
- typeof nodeId === 'number'
- ? await HttpUtil.get(`/panel/api/nodes/webCert/${nodeId}`, undefined, { silent: true })
- : await HttpUtil.post('/panel/api/setting/all', undefined, { silent: true });
- if (!msg?.success) {
- messageApi.warning(msg?.msg || t('pages.inbounds.setDefaultCertEmpty'));
- return;
- }
- const obj = msg.obj as { webCertFile?: string; webKeyFile?: string };
- if (!obj?.webCertFile && !obj?.webKeyFile) {
- messageApi.warning(t('pages.inbounds.setDefaultCertEmpty'));
- return;
- }
- setValue(
- `streamSettings.tlsSettings.certificates.${certName}.certificateFile`,
- obj.webCertFile ?? '',
- );
- setValue(`streamSettings.tlsSettings.certificates.${certName}.keyFile`, obj.webKeyFile ?? '');
- } finally {
- setSaving(false);
- }
- };
- const clearCertFiles = (certName: number) => {
- setValue(`streamSettings.tlsSettings.certificates.${certName}.certificateFile`, '');
- setValue(`streamSettings.tlsSettings.certificates.${certName}.keyFile`, '');
- };
- const onSecurityChange = async (next: string) => {
- setScanResult(null);
- const current = (getValues('streamSettings') as Record<string, unknown>) ?? {};
- const cleaned: Record<string, unknown> = { ...current, security: next };
- delete cleaned.tlsSettings;
- delete cleaned.realitySettings;
- if (next === 'tls') {
- cleaned.tlsSettings = createTlsSettingsWithDefaultCert();
- }
- if (next === 'reality') {
- const reality = RealityStreamSettingsSchema.parse({}) as Record<string, unknown>;
- reality.target = '';
- reality.serverNames = [];
- reality.shortIds = RandomUtil.randomShortIds()
- .split(',')
- .map((s) => s.trim())
- .filter(Boolean);
- cleaned.realitySettings = reality;
- }
- setValue('streamSettings', cleaned);
- if (next === 'reality') {
- randomizeSpiderX();
- try {
- const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
- if (msg?.success) {
- const obj = msg.obj as { privateKey: string; publicKey: string };
- setValue('streamSettings.realitySettings.privateKey', obj.privateKey);
- setValue('streamSettings.realitySettings.settings.publicKey', obj.publicKey);
- }
- } catch {
- /* best-effort: leave keypair fields empty if server call fails */
- }
- }
- };
- return {
- genRealityKeypair,
- clearRealityKeypair,
- genMldsa65,
- clearMldsa65,
- scanRealityTarget,
- scanRealityCandidates,
- applyRealityScanResult,
- randomizeShortIds,
- randomizeSpiderX,
- getNewEchCert,
- clearEchCert,
- pinFromCert,
- pinFromRemote,
- setCertFromPanel,
- clearCertFiles,
- onSecurityChange,
- };
- }
|