diff --git a/frontend/src/pages/inbounds/form/InboundFormModal.tsx b/frontend/src/pages/inbounds/form/InboundFormModal.tsx
index 8e2fc459..f8364ff9 100644
--- a/frontend/src/pages/inbounds/form/InboundFormModal.tsx
+++ b/frontend/src/pages/inbounds/form/InboundFormModal.tsx
@@ -24,7 +24,6 @@ import {
DeleteOutlined,
MinusOutlined,
PlusOutlined,
- ReloadOutlined,
} from '@ant-design/icons';
import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils';
@@ -54,9 +53,6 @@ import {
Protocols,
SNIFFING_OPTION,
TCP_CONGESTION_OPTION,
- TLS_CIPHER_OPTION,
- TLS_VERSION_OPTION,
- USAGE_OPTION,
UTLS_FINGERPRINT,
} from '@/schemas/primitives';
import {
@@ -97,8 +93,8 @@ import {
WsForm,
XhttpForm,
} from './transport';
+import { RealityForm, TlsForm } from './security';
-const { TextArea } = Input;
import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
import type { NodeRecord } from '@/api/queries/useNodesQuery';
@@ -1481,421 +1477,28 @@ export default function InboundFormModal({
-
- prev.streamSettings?.security !== curr.streamSettings?.security
- }
- >
- {({ getFieldValue }) => {
- const sec = getFieldValue(['streamSettings', 'security']);
- if (sec !== 'tls') return null;
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {security === 'tls' && (
+
+ )}
-
- {(certFields, { add, remove }) => (
- <>
-
-
-
- {certFields.map((certField, idx) => (
-
-
-
-
- {t('pages.inbounds.certificatePath')}
-
-
- {t('pages.inbounds.certificateContent')}
-
-
-
- {certFields.length > 1 && (
-
-
-
- )}
-
- prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile
- !== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile
- }
- >
- {({ getFieldValue }) => {
- const useFile = getFieldValue([
- 'streamSettings', 'tlsSettings', 'certificates',
- certField.name, 'useFile',
- ]);
- return useFile ? (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
- >
- ) : (
- <>
- typeof v === 'string'
- ? v.split('\n')
- : v}
- getValueProps={(v) => ({
- value: Array.isArray(v) ? v.join('\n') : v,
- })}
- >
-
-
- typeof v === 'string'
- ? v.split('\n')
- : v}
- getValueProps={(v) => ({
- value: Array.isArray(v) ? v.join('\n') : v,
- })}
- >
-
-
- >
- );
- }}
-
-
-
-
-
-
-
- prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.usage
- !== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.usage
- }
- >
- {({ getFieldValue }) => {
- const usage = getFieldValue([
- 'streamSettings', 'tlsSettings', 'certificates',
- certField.name, 'usage',
- ]);
- if (usage !== 'issue') return null;
- return (
-
-
-
- );
- }}
-
-
- ))}
- >
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- onClick={generateRandomPinHash}
- title={t('pages.inbounds.form.generateRandomPin')}
- />
-
-
-
-
-
-
-
-
- >
- );
- }}
-
-
-
- prev.streamSettings?.security !== curr.streamSettings?.security
- }
- >
- {({ getFieldValue }) => {
- const sec = getFieldValue(['streamSettings', 'security']);
- if (sec !== 'reality') return null;
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
- } onClick={randomizeRealityTarget} />
-
-
-
-
-
-
-
- } onClick={randomizeRealityTarget} />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- } onClick={randomizeShortIds} />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
- }}
-
+ {security === 'reality' && (
+
+ )}
>
);
diff --git a/frontend/src/pages/inbounds/form/security/index.ts b/frontend/src/pages/inbounds/form/security/index.ts
new file mode 100644
index 00000000..f056fc2c
--- /dev/null
+++ b/frontend/src/pages/inbounds/form/security/index.ts
@@ -0,0 +1,2 @@
+export { default as TlsForm } from './tls';
+export { default as RealityForm } from './reality';
diff --git a/frontend/src/pages/inbounds/form/security/reality.tsx b/frontend/src/pages/inbounds/form/security/reality.tsx
new file mode 100644
index 00000000..fa33cdf3
--- /dev/null
+++ b/frontend/src/pages/inbounds/form/security/reality.tsx
@@ -0,0 +1,143 @@
+import { useTranslation } from 'react-i18next';
+import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
+import { ReloadOutlined } from '@ant-design/icons';
+
+import { UTLS_FINGERPRINT } from '@/schemas/primitives';
+
+interface RealityFormProps {
+ saving: boolean;
+ randomizeRealityTarget: () => void;
+ randomizeShortIds: () => void;
+ genRealityKeypair: () => void;
+ clearRealityKeypair: () => void;
+ genMldsa65: () => void;
+ clearMldsa65: () => void;
+}
+
+export default function RealityForm({
+ saving,
+ randomizeRealityTarget,
+ randomizeShortIds,
+ genRealityKeypair,
+ clearRealityKeypair,
+ genMldsa65,
+ clearMldsa65,
+}: RealityFormProps) {
+ const { t } = useTranslation();
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ } onClick={randomizeRealityTarget} />
+
+
+
+
+
+
+
+ } onClick={randomizeRealityTarget} />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ } onClick={randomizeShortIds} />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/pages/inbounds/form/security/tls.tsx b/frontend/src/pages/inbounds/form/security/tls.tsx
new file mode 100644
index 00000000..bcf4b9dd
--- /dev/null
+++ b/frontend/src/pages/inbounds/form/security/tls.tsx
@@ -0,0 +1,309 @@
+import { useTranslation } from 'react-i18next';
+import { Button, Form, Input, Radio, Select, Space, Switch } from 'antd';
+import { MinusOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
+
+import {
+ ALPN_OPTION,
+ TLS_CIPHER_OPTION,
+ TLS_VERSION_OPTION,
+ USAGE_OPTION,
+ UTLS_FINGERPRINT,
+} from '@/schemas/primitives';
+
+const { TextArea } = Input;
+
+interface TlsFormProps {
+ saving: boolean;
+ setCertFromPanel: (certName: number) => void;
+ clearCertFiles: (certName: number) => void;
+ generateRandomPinHash: () => void;
+ getNewEchCert: () => void;
+ clearEchCert: () => void;
+}
+
+export default function TlsForm({
+ saving,
+ setCertFromPanel,
+ clearCertFiles,
+ generateRandomPinHash,
+ getNewEchCert,
+ clearEchCert,
+}: TlsFormProps) {
+ const { t } = useTranslation();
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(certFields, { add, remove }) => (
+ <>
+
+
+
+ {certFields.map((certField, idx) => (
+
+
+
+
+ {t('pages.inbounds.certificatePath')}
+
+
+ {t('pages.inbounds.certificateContent')}
+
+
+
+ {certFields.length > 1 && (
+
+
+
+ )}
+
+ prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile
+ !== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile
+ }
+ >
+ {({ getFieldValue }) => {
+ const useFile = getFieldValue([
+ 'streamSettings', 'tlsSettings', 'certificates',
+ certField.name, 'useFile',
+ ]);
+ return useFile ? (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+ typeof v === 'string'
+ ? v.split('\n')
+ : v}
+ getValueProps={(v) => ({
+ value: Array.isArray(v) ? v.join('\n') : v,
+ })}
+ >
+
+
+ typeof v === 'string'
+ ? v.split('\n')
+ : v}
+ getValueProps={(v) => ({
+ value: Array.isArray(v) ? v.join('\n') : v,
+ })}
+ >
+
+
+ >
+ );
+ }}
+
+
+
+
+
+
+
+ prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.usage
+ !== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.usage
+ }
+ >
+ {({ getFieldValue }) => {
+ const usage = getFieldValue([
+ 'streamSettings', 'tlsSettings', 'certificates',
+ certField.name, 'usage',
+ ]);
+ if (usage !== 'issue') return null;
+ return (
+
+
+
+ );
+ }}
+
+
+ ))}
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ onClick={generateRandomPinHash}
+ title={t('pages.inbounds.form.generateRandomPin')}
+ />
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap b/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap
new file mode 100644
index 00000000..b5f850da
--- /dev/null
+++ b/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap
@@ -0,0 +1,98 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`inbound security forms > RealityForm field structure is stable 1`] = `
+[
+ "Show",
+ "Xver",
+ "uTLS",
+ "Target",
+ "SNI",
+ "Max Time Diff (ms)",
+ "Min Client Ver",
+ "Max Client Ver",
+ "Short IDs",
+ "SpiderX",
+ "Public Key",
+ "Private Key",
+ "mldsa65 Seed",
+ "mldsa65 Verify",
+]
+`;
+
+exports[`inbound security forms > TlsForm field structure is stable 1`] = `
+[
+ "SNI",
+ "Cipher Suites",
+ "Min/Max Version",
+ "uTLS",
+ "ALPN",
+ "Reject Unknown SNI",
+ "Disable System Root",
+ "Session Resumption",
+ "Digital Certificate",
+ "ECH key",
+ "ECH config",
+ "Pinned Peer Cert SHA-256",
+]
+`;
+
+exports[`inbound transport forms > GrpcForm field structure is stable 1`] = `
+[
+ "Service Name",
+ "Authority",
+ "Multi Mode",
+]
+`;
+
+exports[`inbound transport forms > HttpUpgradeForm field structure is stable 1`] = `
+[
+ "Proxy Protocol",
+ "Host",
+ "Path",
+ "Headers",
+]
+`;
+
+exports[`inbound transport forms > KcpForm field structure is stable 1`] = `
+[
+ "MTU",
+ "TTI (ms)",
+ "Uplink (MB/s)",
+ "Downlink (MB/s)",
+ "CWND Multiplier",
+ "Max Sending Window",
+]
+`;
+
+exports[`inbound transport forms > RawForm field structure is stable 1`] = `
+[
+ "Proxy Protocol",
+ "HTTP Obfuscation",
+]
+`;
+
+exports[`inbound transport forms > WsForm field structure is stable 1`] = `
+[
+ "Proxy Protocol",
+ "Host",
+ "Path",
+ "Heartbeat Period",
+ "Headers",
+]
+`;
+
+exports[`inbound transport forms > XhttpForm field structure is stable 1`] = `
+[
+ "Host",
+ "Path",
+ "Mode",
+ "Server Max Header Bytes",
+ "Padding Bytes",
+ "Headers",
+ "Uplink HTTP Method",
+ "Padding Obfs Mode",
+ "Session Placement",
+ "Sequence Placement",
+ "No SSE Header",
+]
+`;
diff --git a/frontend/src/test/inbound-form-blocks.test.tsx b/frontend/src/test/inbound-form-blocks.test.tsx
new file mode 100644
index 00000000..15d376f2
--- /dev/null
+++ b/frontend/src/test/inbound-form-blocks.test.tsx
@@ -0,0 +1,89 @@
+import { describe, it, expect } from 'vitest';
+import { Form, type FormInstance } from 'antd';
+import type { ReactNode } from 'react';
+
+import {
+ GrpcForm,
+ HttpUpgradeForm,
+ KcpForm,
+ RawForm,
+ WsForm,
+ XhttpForm,
+} from '@/pages/inbounds/form/transport';
+import { RealityForm, TlsForm } from '@/pages/inbounds/form/security';
+import type { InboundFormValues } from '@/schemas/forms/inbound-form';
+import { renderWithProviders, fieldLabels } from './test-utils';
+
+function FormHarness({ children }: { children: (form: FormInstance) => ReactNode }) {
+ const [form] = Form.useForm();
+ return ;
+}
+
+function renderInForm(node: (form: FormInstance) => ReactNode) {
+ return renderWithProviders({node});
+}
+
+const noop = () => {};
+
+describe('inbound transport forms', () => {
+ it('RawForm field structure is stable', () => {
+ renderInForm(() => );
+ expect(fieldLabels()).toMatchSnapshot();
+ });
+
+ it('WsForm field structure is stable', () => {
+ renderInForm(() => );
+ expect(fieldLabels()).toMatchSnapshot();
+ });
+
+ it('GrpcForm field structure is stable', () => {
+ renderInForm(() => );
+ expect(fieldLabels()).toMatchSnapshot();
+ });
+
+ it('KcpForm field structure is stable', () => {
+ renderInForm(() => );
+ expect(fieldLabels()).toMatchSnapshot();
+ });
+
+ it('HttpUpgradeForm field structure is stable', () => {
+ renderInForm(() => );
+ expect(fieldLabels()).toMatchSnapshot();
+ });
+
+ it('XhttpForm field structure is stable', () => {
+ renderInForm((form) => );
+ expect(fieldLabels()).toMatchSnapshot();
+ });
+});
+
+describe('inbound security forms', () => {
+ it('TlsForm field structure is stable', () => {
+ renderInForm(() => (
+
+ ));
+ expect(fieldLabels()).toMatchSnapshot();
+ });
+
+ it('RealityForm field structure is stable', () => {
+ renderInForm(() => (
+
+ ));
+ expect(fieldLabels()).toMatchSnapshot();
+ });
+});