mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-09 22:55:00 +03:00
814cda3fb4
Bump xtls/xray-core to 50231eaf (v26.7.11) and the three binary pins (DockerInit.sh, release.yml x2) in lockstep. Adapt the panel to the upstream changes: - Shadowsocks "none"/"plain" and VMess "none"/"zero" were removed from the core. A migration rewrites stored none/plain SS methods to a supported cipher and none/zero VMess security to "auto" (on both the clients column and inbound settings JSON); the SS build-time heal does the same so a row injected after boot cannot brick startup. The removed values are dropped from every frontend option list, schema and adapter, and coerced to "auto" at the Go link/sub/Clash emit sites and both link importers. Fix the CipherType_NONE sentinel that no longer compiles. - Unencrypted vless/trojan outbounds to a public address are now refused by the core. Validate outbounds through the vendored config loader when saving the xray template and when storing/merging outbound subscriptions, so one such outbound cannot keep the core from starting. - New TCP finalmask type "xmc" (Minecraft mimicry): add it to the sub link allowlist, the frontend enum and the FinalMask form (hostname, usernames, required password), and document it. - streamSettings gained a "method" alias for "network"; canonicalize it to "network" at inbound save time and in the form adapters/schema so a method-keyed config keeps its transport. - New root "env" config key is passed through xray.Config, compared in Equals, and forces a restart in the hot diff. - REALITY now defaults minClientVer to 26.3.27; update the form placeholder.
261 lines
9.8 KiB
TypeScript
261 lines
9.8 KiB
TypeScript
import { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Alert, Button, Collapse, Descriptions, Divider, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
|
|
import { RadarChartOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
|
|
|
|
import { FormField } from '@/components/form/rhf';
|
|
import { UTLS_FINGERPRINT } from '@/schemas/primitives';
|
|
import { validateRealityTarget } from '@/lib/xray/stream-wire-normalize';
|
|
import type { RealityScanResult } from '@/generated/types';
|
|
import RealityTargetScannerModal from './RealityTargetScannerModal';
|
|
|
|
interface RealityFormProps {
|
|
saving: boolean;
|
|
scanning: boolean;
|
|
scanResult: RealityScanResult | null;
|
|
scanRealityTarget: () => void;
|
|
scanRealityCandidates: (targets?: string) => Promise<RealityScanResult[]>;
|
|
applyRealityScanResult: (result: RealityScanResult) => void;
|
|
randomizeShortIds: () => void;
|
|
randomizeSpiderX: () => void;
|
|
genRealityKeypair: () => void;
|
|
clearRealityKeypair: () => void;
|
|
genMldsa65: () => void;
|
|
clearMldsa65: () => void;
|
|
}
|
|
|
|
export default function RealityForm({
|
|
saving,
|
|
scanning,
|
|
scanResult,
|
|
scanRealityTarget,
|
|
scanRealityCandidates,
|
|
applyRealityScanResult,
|
|
randomizeShortIds,
|
|
randomizeSpiderX,
|
|
genRealityKeypair,
|
|
clearRealityKeypair,
|
|
genMldsa65,
|
|
clearMldsa65,
|
|
}: RealityFormProps) {
|
|
const { t } = useTranslation();
|
|
const [scannerOpen, setScannerOpen] = useState(false);
|
|
return (
|
|
<>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', 'show']}
|
|
label={t('pages.inbounds.form.show')}
|
|
valueProp="checked"
|
|
>
|
|
<Switch />
|
|
</FormField>
|
|
<FormField name={['streamSettings', 'realitySettings', 'xver']} label={t('pages.inbounds.form.xver')}>
|
|
<InputNumber min={0} />
|
|
</FormField>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', 'settings', 'fingerprint']}
|
|
label="uTLS"
|
|
>
|
|
<Select
|
|
options={Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp }))}
|
|
/>
|
|
</FormField>
|
|
<Form.Item
|
|
label={t('pages.inbounds.form.target')}
|
|
tooltip={t('pages.inbounds.form.realityTargetHint')}
|
|
>
|
|
<Space.Compact block style={{ display: 'flex' }}>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', 'target']}
|
|
noStyle
|
|
rules={{
|
|
validate: (value) => {
|
|
const errKey = validateRealityTarget(typeof value === 'string' ? value : '');
|
|
return errKey ? errKey : true;
|
|
},
|
|
}}
|
|
>
|
|
<Input style={{ flex: 1 }} placeholder="example.com:443" />
|
|
</FormField>
|
|
<Button icon={<RadarChartOutlined />} loading={scanning} onClick={scanRealityTarget}>
|
|
{t('pages.inbounds.form.scan')}
|
|
</Button>
|
|
<Button icon={<SearchOutlined />} onClick={() => setScannerOpen(true)}>
|
|
{t('pages.inbounds.form.findTargets')}
|
|
</Button>
|
|
</Space.Compact>
|
|
</Form.Item>
|
|
{scanResult && (
|
|
<Form.Item label=" " colon={false}>
|
|
<Alert
|
|
type={scanResult.feasible ? 'success' : 'warning'}
|
|
showIcon
|
|
title={
|
|
scanResult.feasible
|
|
? t('pages.inbounds.form.scanFeasible')
|
|
: scanResult.reason || t('pages.inbounds.form.scanNotFeasible')
|
|
}
|
|
description={
|
|
<Descriptions size="small" column={1}>
|
|
<Descriptions.Item label="TLS">{scanResult.tlsVersion || '—'}</Descriptions.Item>
|
|
<Descriptions.Item label="ALPN">{scanResult.alpn || '—'}</Descriptions.Item>
|
|
<Descriptions.Item label={t('pages.inbounds.form.scanCurve')}>
|
|
{scanResult.curveID || '—'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label={t('pages.inbounds.form.scanCert')}>
|
|
{scanResult.certValid
|
|
? `${scanResult.certSubject} (${scanResult.certIssuer})`
|
|
: t('pages.inbounds.form.scanCertInvalid')}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label={t('pages.inbounds.form.scanLatency')}>
|
|
{scanResult.latencyMs > 0 ? `${scanResult.latencyMs} ms` : '—'}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
}
|
|
/>
|
|
</Form.Item>
|
|
)}
|
|
<FormField label="SNI" name={['streamSettings', 'realitySettings', 'serverNames']}>
|
|
<Select mode="tags" tokenSeparators={[',']} style={{ width: '100%' }} />
|
|
</FormField>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', 'maxTimediff']}
|
|
label={t('pages.inbounds.form.maxTimeDiff')}
|
|
>
|
|
<InputNumber min={0} />
|
|
</FormField>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', 'minClientVer']}
|
|
label={t('pages.inbounds.form.minClientVer')}
|
|
>
|
|
<Input placeholder="26.3.27" />
|
|
</FormField>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', 'maxClientVer']}
|
|
label={t('pages.inbounds.form.maxClientVer')}
|
|
>
|
|
<Input placeholder="25.9.11" />
|
|
</FormField>
|
|
<Form.Item label={t('pages.inbounds.form.shortIds')}>
|
|
<Space.Compact block style={{ display: 'flex' }}>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', 'shortIds']}
|
|
noStyle
|
|
>
|
|
<Select mode="tags" tokenSeparators={[',']} style={{ flex: 1 }} />
|
|
</FormField>
|
|
<Button aria-label={t('regenerate')} icon={<ReloadOutlined />} onClick={randomizeShortIds} />
|
|
</Space.Compact>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('pages.inbounds.form.spiderX')}
|
|
tooltip={t('pages.inbounds.form.spiderXHint')}
|
|
>
|
|
<Space.Compact block style={{ display: 'flex' }}>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', 'settings', 'spiderX']}
|
|
noStyle
|
|
>
|
|
<Input style={{ flex: 1 }} />
|
|
</FormField>
|
|
<Button aria-label={t('regenerate')} icon={<ReloadOutlined />} onClick={randomizeSpiderX} />
|
|
</Space.Compact>
|
|
</Form.Item>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', 'settings', 'publicKey']}
|
|
label={t('pages.inbounds.publicKey')}
|
|
>
|
|
<Input.TextArea autoSize={{ minRows: 1, maxRows: 4 }} />
|
|
</FormField>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', 'privateKey']}
|
|
label={t('pages.inbounds.privatekey')}
|
|
>
|
|
<Input.TextArea autoSize={{ minRows: 1, maxRows: 4 }} />
|
|
</FormField>
|
|
<Form.Item label=" ">
|
|
<Space>
|
|
<Button type="primary" loading={saving} onClick={genRealityKeypair}>
|
|
{t('pages.inbounds.form.getNewCert')}
|
|
</Button>
|
|
<Button danger onClick={clearRealityKeypair}>{t('clear')}</Button>
|
|
</Space>
|
|
</Form.Item>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', 'mldsa65Seed']}
|
|
label={t('pages.inbounds.form.mldsa65Seed')}
|
|
>
|
|
<Input.TextArea autoSize={{ minRows: 2, maxRows: 6 }} />
|
|
</FormField>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify']}
|
|
label={t('pages.inbounds.form.mldsa65Verify')}
|
|
>
|
|
<Input.TextArea autoSize={{ minRows: 2, maxRows: 6 }} />
|
|
</FormField>
|
|
<Form.Item label=" ">
|
|
<Space>
|
|
<Button type="primary" loading={saving} onClick={genMldsa65}>
|
|
{t('pages.inbounds.form.getNewSeed')}
|
|
</Button>
|
|
<Button danger onClick={clearMldsa65}>{t('clear')}</Button>
|
|
</Space>
|
|
</Form.Item>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', 'masterKeyLog']}
|
|
label={t('pages.inbounds.form.masterKeyLog')}
|
|
tooltip={t('pages.inbounds.form.masterKeyLogTip')}
|
|
>
|
|
<Input placeholder="/path/to/sslkeylog.txt" />
|
|
</FormField>
|
|
<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>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', dir, 'afterBytes']}
|
|
label={t('pages.inbounds.form.afterBytes')}
|
|
tooltip={t('pages.inbounds.form.afterBytesTip')}
|
|
>
|
|
<InputNumber min={0} />
|
|
</FormField>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', dir, 'bytesPerSec']}
|
|
label={t('pages.inbounds.form.bytesPerSec')}
|
|
tooltip={t('pages.inbounds.form.bytesPerSecTip')}
|
|
>
|
|
<InputNumber min={0} />
|
|
</FormField>
|
|
<FormField
|
|
name={['streamSettings', 'realitySettings', dir, 'burstBytesPerSec']}
|
|
label={t('pages.inbounds.form.burstBytesPerSec')}
|
|
tooltip={t('pages.inbounds.form.burstBytesPerSecTip')}
|
|
>
|
|
<InputNumber min={0} />
|
|
</FormField>
|
|
</div>
|
|
))}
|
|
</>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
<RealityTargetScannerModal
|
|
open={scannerOpen}
|
|
onClose={() => setScannerOpen(false)}
|
|
scanRealityCandidates={scanRealityCandidates}
|
|
onPick={applyRealityScanResult}
|
|
/>
|
|
</>
|
|
);
|
|
}
|