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,8 +1,7 @@
import { useTranslation } from 'react-i18next';
import { Button, Card, Empty, Input, InputNumber, Select, Space } from 'antd';
import { Button, Card, Col, Empty, Input, InputNumber, Row, Select, Space } from 'antd';
import { ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import { InputAddon } from '@/components/ui';
import type { FallbackRow } from '@/schemas/forms/inbound-form';
interface FallbacksCardProps {
@@ -25,100 +24,120 @@ export default function FallbacksCard({
addAllFallbacks,
}: FallbacksCardProps) {
const { t } = useTranslation();
const addButtons = (
<Space size={8} wrap>
<Button type="primary" ghost size="small" icon={<PlusOutlined />} onClick={addFallback}>
{t('pages.inbounds.fallbacks.add') || 'Add fallback'}
</Button>
<Button
size="small"
onClick={addAllFallbacks}
disabled={fallbackChildOptions.length === 0 || fallbacks.length >= fallbackChildOptions.length}
title={t('pages.inbounds.form.addAllFallbackTooltip')}
>
{t('pages.inbounds.form.addAll')}
</Button>
</Space>
);
return (
<Card size="small" className="mt-12" title={t('pages.inbounds.fallbacks.title') || 'Fallbacks'}>
{fallbacks.length === 0 && (
<Card
size="small"
className="mt-12"
title={t('pages.inbounds.fallbacks.title') || 'Fallbacks'}
extra={addButtons}
>
{fallbacks.length === 0 ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
styles={{ image: { height: 36 } }}
description={t('pages.inbounds.fallbacks.empty') || 'No fallbacks yet'}
styles={{ image: { height: 40 } }}
style={{ margin: '8px 0 12px' }}
style={{ margin: '4px 0 12px' }}
/>
) : (
fallbacks.map((record, idx) => (
<Card
key={record.rowKey}
type="inner"
size="small"
style={{ marginBottom: 8 }}
styles={{ body: { padding: 12 } }}
>
<Space.Compact block style={{ marginBottom: 8 }}>
<Select
value={record.childId}
options={fallbackChildOptions}
placeholder={t('pages.inbounds.fallbacks.pickInbound') || 'Pick an inbound'}
allowClear
showSearch={{
filterOption: (input, option) =>
((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
}}
style={{ width: '100%' }}
onChange={(v) => updateFallback(record.rowKey, { childId: v ?? null })}
/>
<Button
disabled={idx === 0}
onClick={() => moveFallback(idx, -1)}
title={t('pages.inbounds.form.moveUp')}
icon={<ArrowUpOutlined />}
/>
<Button
disabled={idx === fallbacks.length - 1}
onClick={() => moveFallback(idx, 1)}
title={t('pages.inbounds.form.moveDown')}
icon={<ArrowDownOutlined />}
/>
<Button danger onClick={() => removeFallback(idx)} icon={<DeleteOutlined />} />
</Space.Compact>
<Row gutter={[8, 8]}>
<Col xs={24} sm={12}>
<Input
addonBefore="SNI"
placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
value={record.name}
onChange={(e) => updateFallback(record.rowKey, { name: e.target.value })}
/>
</Col>
<Col xs={24} sm={12}>
<Input
addonBefore="ALPN"
placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
value={record.alpn}
onChange={(e) => updateFallback(record.rowKey, { alpn: e.target.value })}
/>
</Col>
<Col xs={24} sm={12}>
<Input
addonBefore="Path"
placeholder="/"
value={record.path}
onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })}
/>
</Col>
<Col xs={24} sm={12}>
<Input
addonBefore="Dest"
placeholder={t('pages.inbounds.fallbacks.destPlaceholder') || 'auto'}
value={record.dest}
onChange={(e) => updateFallback(record.rowKey, { dest: e.target.value })}
/>
</Col>
<Col xs={24} sm={12}>
<InputNumber
addonBefore="xver"
min={0}
max={2}
style={{ width: '100%' }}
value={record.xver}
onChange={(v) => updateFallback(record.rowKey, { xver: Number(v) || 0 })}
/>
</Col>
</Row>
</Card>
))
)}
{fallbacks.map((record, idx) => (
<div
key={record.rowKey}
style={{ border: '1px solid var(--app-border-tertiary)', borderRadius: 6, padding: '10px 12px', marginBottom: 8 }}
>
<Space.Compact block style={{ marginBottom: 6 }}>
<Select
value={record.childId}
options={fallbackChildOptions}
placeholder={t('pages.inbounds.fallbacks.pickInbound') || 'Pick an inbound'}
allowClear
showSearch={{
filterOption: (input, option) =>
((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
}}
style={{ width: '100%' }}
onChange={(v) => updateFallback(record.rowKey, { childId: v ?? null })}
/>
<Button
disabled={idx === 0}
onClick={() => moveFallback(idx, -1)}
title={t('pages.inbounds.form.moveUp')}
>
<ArrowUpOutlined />
</Button>
<Button
disabled={idx === fallbacks.length - 1}
onClick={() => moveFallback(idx, 1)}
title={t('pages.inbounds.form.moveDown')}
>
<ArrowDownOutlined />
</Button>
<Button danger onClick={() => removeFallback(idx)}>
<DeleteOutlined />
</Button>
</Space.Compact>
<Space.Compact block>
<InputAddon>SNI</InputAddon>
<Input
placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
value={record.name}
onChange={(e) => updateFallback(record.rowKey, { name: e.target.value })}
/>
<InputAddon>ALPN</InputAddon>
<Input
placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
value={record.alpn}
onChange={(e) => updateFallback(record.rowKey, { alpn: e.target.value })}
/>
<InputAddon>Path</InputAddon>
<Input
placeholder="/"
value={record.path}
onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })}
/>
<InputAddon>Dest</InputAddon>
<Input
placeholder={t('pages.inbounds.fallbacks.destPlaceholder') || 'auto'}
value={record.dest}
onChange={(e) => updateFallback(record.rowKey, { dest: e.target.value })}
/>
<InputAddon>xver</InputAddon>
<InputNumber
min={0}
max={2}
value={record.xver}
onChange={(v) => updateFallback(record.rowKey, { xver: Number(v) || 0 })}
/>
</Space.Compact>
</div>
))}
<Space>
<Button size="small" onClick={addFallback}>
<PlusOutlined /> {t('pages.inbounds.fallbacks.add') || 'Add fallback'}
</Button>
<Button
size="small"
onClick={addAllFallbacks}
disabled={fallbackChildOptions.length === 0
|| fallbacks.length >= fallbackChildOptions.length}
title={t('pages.inbounds.form.addAllFallbackTooltip')}
>
{t('pages.inbounds.form.addAll')}
</Button>
</Space>
</Card>
);
}