feat(tls,reality): port xray TLS/REALITY fields, cert-hash helpers, fallback UX

TLS: add verifyPeerCertByName (vcn) to inbound settings + emit in both share-link generators (frontend + Go sub) and outbound parser; the allowInsecure replacement xray removed after 2026-06-01. Add server-side curvePreferences, masterKeyLog, echSockopt (passthrough + form) at tlsSettings top-level so they survive the panel-only settings strip.

REALITY: add limitFallbackUpload/Download (afterBytes/bytesPerSec/burstBytesPerSec) with per-field tooltips, plus masterKeyLog. Verified field names/semantics against pinned xray v1.260327.1 (bytesPerSec=0 disables).

Hosts: fix verify_peer_cert_by_name column bool->string (xray expects comma-separated names) with an idempotent, history-gate-free migration (SQLite typeof blank; Postgres ALTER once); emit vcn for hosts/external proxies.

Server: add getCertHash (local cert DER SHA-256) and getRemoteCertHash (xray tls ping) endpoints + api-docs; wire pinned-cert field buttons. Drop the meaningless random-hash button.

Xray UI: metrics endpoint (listen/tag) config in Basics; import/export for routing rules and outbounds.

Fallbacks card: compact empty state, header-aligned actions, responsive labeled grid rows.

i18n: add all new keys to every locale; drop unused generateRandomPin.
This commit is contained in:
MHSanaei
2026-06-21 15:51:50 +02:00
parent 315ecc2588
commit 7c8889466b
48 changed files with 1316 additions and 173 deletions
@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next';
import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
import { Button, Collapse, Divider, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import { UTLS_FINGERPRINT } from '@/schemas/primitives';
@@ -153,6 +153,54 @@ export default function RealityForm({
<Button danger onClick={clearMldsa65}>{t('clear')}</Button>
</Space>
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'masterKeyLog']}
label={t('pages.inbounds.form.masterKeyLog')}
tooltip={t('pages.inbounds.form.masterKeyLogTip')}
>
<Input placeholder="/path/to/sslkeylog.txt" />
</Form.Item>
<Collapse
style={{ marginBottom: 14 }}
items={[
{
key: 'limitFallback',
label: t('pages.inbounds.form.limitFallback'),
children: (
<>
{(['limitFallbackUpload', 'limitFallbackDownload'] as const).map((dir) => (
<div key={dir}>
<Divider style={{ margin: '0 0 14px 0' }}>
{t(`pages.inbounds.form.${dir}`)}
</Divider>
<Form.Item
name={['streamSettings', 'realitySettings', dir, 'afterBytes']}
label={t('pages.inbounds.form.afterBytes')}
tooltip={t('pages.inbounds.form.afterBytesTip')}
>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', dir, 'bytesPerSec']}
label={t('pages.inbounds.form.bytesPerSec')}
tooltip={t('pages.inbounds.form.bytesPerSecTip')}
>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', dir, 'burstBytesPerSec']}
label={t('pages.inbounds.form.burstBytesPerSec')}
tooltip={t('pages.inbounds.form.burstBytesPerSecTip')}
>
<InputNumber min={0} />
</Form.Item>
</div>
))}
</>
),
},
]}
/>
</>
);
}
+112 -15
View File
@@ -1,14 +1,16 @@
import { useTranslation } from 'react-i18next';
import { Button, Form, Input, InputNumber, Radio, Select, Space, Switch } from 'antd';
import { MinusOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
import { CloudDownloadOutlined, FileProtectOutlined, MinusOutlined, PlusOutlined } from '@ant-design/icons';
import {
ALPN_OPTION,
DOMAIN_STRATEGY_OPTION,
TLS_CIPHER_OPTION,
TLS_VERSION_OPTION,
USAGE_OPTION,
UTLS_FINGERPRINT,
} from '@/schemas/primitives';
import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
const { TextArea } = Input;
@@ -16,7 +18,8 @@ interface TlsFormProps {
saving: boolean;
setCertFromPanel: (certName: number) => void;
clearCertFiles: (certName: number) => void;
generateRandomPinHash: () => void;
pinFromCert: () => void;
pinFromRemote: () => void;
getNewEchCert: () => void;
clearEchCert: () => void;
}
@@ -25,7 +28,8 @@ export default function TlsForm({
saving,
setCertFromPanel,
clearCertFiles,
generateRandomPinHash,
pinFromCert,
pinFromRemote,
getNewEchCert,
clearEchCert,
}: TlsFormProps) {
@@ -78,6 +82,21 @@ export default function TlsForm({
options={Object.values(ALPN_OPTION).map((a) => ({ value: a, label: a }))}
/>
</Form.Item>
<Form.Item
name={['streamSettings', 'tlsSettings', 'curvePreferences']}
label={t('pages.inbounds.form.curvePreferences')}
tooltip={t('pages.inbounds.form.curvePreferencesTip')}
>
<Select
mode="tags"
tokenSeparators={[',', ' ']}
style={{ width: '100%' }}
options={['X25519MLKEM768', 'X25519', 'P-256', 'P-384', 'P-521'].map((c) => ({
value: c,
label: c,
}))}
/>
</Form.Item>
<Form.Item
name={['streamSettings', 'tlsSettings', 'rejectUnknownSni']}
label={t('pages.inbounds.form.rejectUnknownSni')}
@@ -270,7 +289,71 @@ export default function TlsForm({
</>
)}
</Form.List>
<Form.Item
name={['streamSettings', 'tlsSettings', 'masterKeyLog']}
label={t('pages.inbounds.form.masterKeyLog')}
tooltip={t('pages.inbounds.form.masterKeyLogTip')}
>
<Input placeholder="/path/to/sslkeylog.txt" />
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prev, curr) =>
!!(prev.streamSettings as { tlsSettings?: { echSockopt?: unknown } } | undefined)?.tlsSettings?.echSockopt
!== !!(curr.streamSettings as { tlsSettings?: { echSockopt?: unknown } } | undefined)?.tlsSettings?.echSockopt
}
>
{({ getFieldValue, setFieldValue }) => {
const on = !!getFieldValue(['streamSettings', 'tlsSettings', 'echSockopt']);
return (
<>
<Form.Item label={t('pages.inbounds.form.echSockopt')} tooltip={t('pages.inbounds.form.echSockoptTip')}>
<Switch
checked={on}
onChange={(v) =>
setFieldValue(
['streamSettings', 'tlsSettings', 'echSockopt'],
v ? SockoptStreamSettingsSchema.parse({}) : undefined,
)
}
/>
</Form.Item>
{on && (
<>
<Form.Item
name={['streamSettings', 'tlsSettings', 'echSockopt', 'dialerProxy']}
label={t('pages.inbounds.form.dialerProxy')}
>
<Input />
</Form.Item>
<Form.Item
name={['streamSettings', 'tlsSettings', 'echSockopt', 'domainStrategy']}
label={t('pages.xray.wireguard.domainStrategy')}
>
<Select
options={Object.values(DOMAIN_STRATEGY_OPTION).map((v) => ({ value: v, label: v }))}
/>
</Form.Item>
<Form.Item
name={['streamSettings', 'tlsSettings', 'echSockopt', 'tcpFastOpen']}
label={t('pages.inbounds.form.tcpFastOpen')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['streamSettings', 'tlsSettings', 'echSockopt', 'tcpMptcp']}
label={t('pages.inbounds.form.multipathTcp')}
valuePropName="checked"
>
<Switch />
</Form.Item>
</>
)}
</>
);
}}
</Form.Item>
<Form.Item name={['streamSettings', 'tlsSettings', 'echServerKeys']} label={t('pages.inbounds.form.echKey')}>
<Input />
</Form.Item>
@@ -280,6 +363,14 @@ export default function TlsForm({
>
<Input />
</Form.Item>
<Form.Item label=" ">
<Space>
<Button type="primary" loading={saving} onClick={getNewEchCert}>
{t('pages.inbounds.form.getNewEchCert')}
</Button>
<Button danger onClick={clearEchCert}>{t('clear')}</Button>
</Space>
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.pinnedPeerCertSha256')}
tooltip={t('pages.inbounds.form.pinnedPeerCertSha256Tip')}
@@ -293,23 +384,29 @@ export default function TlsForm({
mode="tags"
tokenSeparators={[',', ' ']}
placeholder={t('pages.inbounds.form.pinnedPeerCertSha256Placeholder')}
style={{ width: 'calc(100% - 32px)' }}
style={{ width: 'calc(100% - 64px)' }}
/>
</Form.Item>
<Button
icon={<ReloadOutlined />}
onClick={generateRandomPinHash}
title={t('pages.inbounds.form.generateRandomPin')}
icon={<FileProtectOutlined />}
onClick={pinFromCert}
loading={saving}
title={t('pages.inbounds.form.pinFromCert')}
/>
<Button
icon={<CloudDownloadOutlined />}
onClick={pinFromRemote}
loading={saving}
title={t('pages.inbounds.form.pinFromRemote')}
/>
</Space.Compact>
</Form.Item>
<Form.Item label=" ">
<Space>
<Button type="primary" loading={saving} onClick={getNewEchCert}>
{t('pages.inbounds.form.getNewEchCert')}
</Button>
<Button danger onClick={clearEchCert}>{t('clear')}</Button>
</Space>
<Form.Item
name={['streamSettings', 'tlsSettings', 'settings', 'verifyPeerCertByName']}
label={t('pages.inbounds.form.verifyPeerCertByName')}
tooltip={t('pages.inbounds.form.verifyPeerCertByNameTip')}
>
<Input placeholder="example.com" />
</Form.Item>
</>
);