mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
feat(tls): add a dynamic DoH helper for echConfigList, with CDN guidance (#6869)
echConfigList already accepted Xray's dynamic form, target domain + DoH URL, but nothing in the inbound or host forms explained or built it. A helper now builds that value from presets or a custom domain and resolver, accepts only the schemes Xray dials (https, h2c, udp; scheme case normalised), warns that a key generated locally does not work behind a CDN, and asks before overwriting local ECH keys. The helper's controls carry accessible labels, and the resolver list holds only identified public resolvers.
This commit is contained in:
@@ -1,6 +1,17 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, InputNumber, Modal, Select, Switch, Tabs, message } from 'antd';
|
||||
import {
|
||||
Alert,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Select,
|
||||
Switch,
|
||||
Tabs,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd';
|
||||
import {
|
||||
ProfileOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
@@ -18,6 +29,7 @@ import { BulkAddHostSchema, type BulkAddHostValues } from '@/schemas/api/host';
|
||||
import type { InboundOption } from '@/schemas/client';
|
||||
import { ALPN_OPTION, UTLS_FINGERPRINT } from '@/schemas/primitives';
|
||||
import { CipherSuitesSelect } from '@/components/form';
|
||||
import DynamicEchHelper from '@/components/form/DynamicEchHelper';
|
||||
import { FormField, rhfZodValidate } from '@/components/form/rhf';
|
||||
import { useNodesQuery } from '@/api/queries/useNodesQuery';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
@@ -94,6 +106,8 @@ export default function HostFormModal({
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const security = (useWatch({ control: methods.control, name: 'security' }) ?? 'same') as string;
|
||||
const hostEchConfig = useWatch({ control: methods.control, name: 'echConfigList' }) ?? '';
|
||||
const hostSni = useWatch({ control: methods.control, name: 'sni' }) ?? '';
|
||||
const showTls = security === 'tls' || security === 'reality' || security === 'same';
|
||||
const showTlsExtras = security === 'tls' || security === 'same';
|
||||
const watchedHosts = useWatch({ control: methods.control, name: 'hosts' });
|
||||
@@ -372,10 +386,34 @@ export default function HostFormModal({
|
||||
</FormField>
|
||||
<FormField
|
||||
name="echConfigList"
|
||||
label={t('pages.hosts.fields.echConfigList')}
|
||||
label={t('pages.inbounds.form.echConfigList')}
|
||||
extra={
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{t('pages.inbounds.form.echConfigListHint')}
|
||||
</Typography.Text>
|
||||
}
|
||||
>
|
||||
<Input.TextArea rows={2} />
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder={t('pages.inbounds.form.echDynamicPlaceholder')}
|
||||
/>
|
||||
</FormField>
|
||||
<Form.Item label=" " style={{ marginBottom: 8 }}>
|
||||
<DynamicEchHelper
|
||||
value={hostEchConfig}
|
||||
sni={hostSni}
|
||||
onApply={(next) =>
|
||||
methods.setValue('echConfigList', next, { shouldDirty: true })
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
data-testid="ech-cdn-warning"
|
||||
title={t('pages.inbounds.form.echCdnWarningTitle')}
|
||||
description={t('pages.inbounds.form.echCdnWarningBody')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Input, InputNumber, Radio, Select, Space, Switch } from 'antd';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
CloudDownloadOutlined,
|
||||
FileProtectOutlined,
|
||||
@@ -10,6 +21,7 @@ import {
|
||||
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
import { CipherSuitesSelect } from '@/components/form';
|
||||
import DynamicEchHelper from '@/components/form/DynamicEchHelper';
|
||||
import { FormField } from '@/components/form/rhf';
|
||||
import {
|
||||
ALPN_OPTION,
|
||||
@@ -224,6 +236,79 @@ function EchSockoptSection() {
|
||||
);
|
||||
}
|
||||
|
||||
interface EchKeySectionProps {
|
||||
saving: boolean;
|
||||
getNewEchCert: () => void;
|
||||
clearEchCert: () => void;
|
||||
}
|
||||
|
||||
function EchKeySection({ saving, getNewEchCert, clearEchCert }: EchKeySectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const { control, setValue } = useFormContext();
|
||||
const localKeys = useWatch({ control, name: 'streamSettings.tlsSettings.echServerKeys' });
|
||||
const echConfig = useWatch({
|
||||
control,
|
||||
name: 'streamSettings.tlsSettings.settings.echConfigList',
|
||||
});
|
||||
const serverName: string =
|
||||
useWatch({ control, name: 'streamSettings.tlsSettings.serverName' }) ?? '';
|
||||
const helper = (
|
||||
<DynamicEchHelper
|
||||
value={echConfig ?? ''}
|
||||
sni={serverName}
|
||||
hasLocalKeys={!!localKeys}
|
||||
onApply={(next) =>
|
||||
setValue('streamSettings.tlsSettings.settings.echConfigList', next, {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
onClearLocalKeys={() => setValue('streamSettings.tlsSettings.echServerKeys', '')}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
name={['streamSettings', 'tlsSettings', 'settings', 'echConfigList']}
|
||||
label={t('pages.inbounds.form.echConfigList')}
|
||||
extra={
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{t('pages.inbounds.form.echConfigListHint')}
|
||||
</Typography.Text>
|
||||
}
|
||||
>
|
||||
<Input placeholder={t('pages.inbounds.form.echDynamicPlaceholder')} />
|
||||
</FormField>
|
||||
<Form.Item label=" " style={{ marginBottom: 8 }}>
|
||||
{helper}
|
||||
</Form.Item>
|
||||
<FormField
|
||||
name={['streamSettings', 'tlsSettings', 'echServerKeys']}
|
||||
label={t('pages.inbounds.form.echKey')}
|
||||
>
|
||||
<Input placeholder={t('pages.inbounds.form.echKeyHint')} />
|
||||
</FormField>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
data-testid="ech-cdn-warning"
|
||||
style={{ marginBottom: 12 }}
|
||||
title={t('pages.inbounds.form.echCdnWarningTitle')}
|
||||
description={t('pages.inbounds.form.echCdnWarningBody')}
|
||||
/>
|
||||
<Form.Item label=" ">
|
||||
<Space wrap>
|
||||
<Button type="primary" loading={saving} onClick={getNewEchCert}>
|
||||
{t('pages.inbounds.form.getNewEchCert')}
|
||||
</Button>
|
||||
<Button danger onClick={clearEchCert}>
|
||||
{t('clear')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TlsForm({
|
||||
saving,
|
||||
setCertFromPanel,
|
||||
@@ -363,28 +448,7 @@ export default function TlsForm({
|
||||
<Input placeholder="/path/to/sslkeylog.txt" />
|
||||
</FormField>
|
||||
<EchSockoptSection />
|
||||
<FormField
|
||||
name={['streamSettings', 'tlsSettings', 'echServerKeys']}
|
||||
label={t('pages.inbounds.form.echKey')}
|
||||
>
|
||||
<Input />
|
||||
</FormField>
|
||||
<FormField
|
||||
name={['streamSettings', 'tlsSettings', 'settings', 'echConfigList']}
|
||||
label={t('pages.inbounds.form.echConfig')}
|
||||
>
|
||||
<Input />
|
||||
</FormField>
|
||||
<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>
|
||||
<EchKeySection saving={saving} getNewEchCert={getNewEchCert} clearEchCert={clearEchCert} />
|
||||
<Form.Item
|
||||
htmlFor={`${uid}-pinnedPeerCertSha256`}
|
||||
label={t('pages.inbounds.form.pinnedPeerCertSha256')}
|
||||
|
||||
Reference in New Issue
Block a user