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:
55575455
2026-10-24 06:07:11 +02:00
committed by GitHub
parent 41bafb3fd5
commit f43645656c
21 changed files with 1380 additions and 53 deletions
@@ -0,0 +1,274 @@
import { useId, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Button, Divider, Form, Input, Select, Space, Typography } from 'antd';
import { ThunderboltOutlined } from '@ant-design/icons';
import {
DEFAULT_DOH_ENDPOINT,
DEFAULT_ECH_DOMAIN,
DEFAULT_ECH_PAIR,
dohEndpointOptions,
echDomainOptions,
formatDynamicEch,
isSupportedDohUrl,
pairPresetOptions,
parseDynamicEch,
} from '@/lib/xray/ech-presets';
interface DynamicEchHelperProps {
/** Current echConfigList, so an existing dynamic pair can be recovered. */
value?: string;
/** Node SNI, used by the "resolve from the proxied hostname" target. */
sni?: string;
/** True when a locally generated ECH keypair is present. */
hasLocalKeys?: boolean;
onApply?: (value: string) => void;
/** Called when the operator accepts dropping CDN-invalid local keys. */
onClearLocalKeys?: () => void;
disabled?: boolean;
}
const CUSTOM = '__custom__';
const AUTO = '__auto__';
const ADVANCED = '__advanced__';
/* Only echConfigList can hold the dynamic `domain+DoH` form; clearing local keys
* is a separate, explicitly confirmed step because behind a CDN they are dead. */
export default function DynamicEchHelper({
value = '',
sni = '',
hasLocalKeys = false,
onApply,
onClearLocalKeys,
disabled = false,
}: DynamicEchHelperProps) {
const { t } = useTranslation();
const uid = useId();
const parsed = parseDynamicEch(value);
const presetValues = new Set(
pairPresetOptions(t).flatMap((group) => group.options.map((o) => o.value)),
);
const [preset, setPreset] = useState<string>(() => {
const current = value.trim();
if (presetValues.has(current)) return current;
if (!current || !parsed) return DEFAULT_ECH_PAIR;
return ADVANCED;
});
const [domainChoice, setDomainChoice] = useState<string>(
() => parsed?.domain ?? DEFAULT_ECH_DOMAIN,
);
const [domainInput, setDomainInput] = useState<string>(() => parsed?.domain ?? '');
const [dohChoice, setDohChoice] = useState<string>(() => parsed?.dohUrl ?? DEFAULT_DOH_ENDPOINT);
const [dohInput, setDohInput] = useState<string>(parsed?.dohUrl ?? '');
const [preview, setPreview] = useState<string | null>(null);
const domain =
domainChoice === CUSTOM ? domainInput : domainChoice === AUTO ? sni.trim() : domainChoice;
const doh = dohChoice === CUSTOM ? dohInput : dohChoice;
const advancedBuilt = formatDynamicEch(domain, doh);
const built = preset === ADVANCED ? advancedBuilt : preset;
const apply = () => {
if (!built) return;
if (hasLocalKeys) setPreview(built);
else onApply?.(built);
};
const pickPreset = (next: string) => {
setPreset(next);
setPreview(null);
};
const pickDomain = (next: string) => {
setDomainChoice(next);
setPreview(null);
};
const pickDoh = (next: string) => {
setDohChoice(next);
setPreview(null);
};
const confirmOverwrite = () => {
if (!built) return;
onApply?.(built);
onClearLocalKeys?.();
setPreview(null);
};
return (
<div data-testid="ech-dynamic-helper" style={{ maxWidth: 640 }}>
<div
style={{ display: 'flex', alignItems: 'center', gap: 6, fontWeight: 600, marginBottom: 4 }}
>
<ThunderboltOutlined aria-hidden />
{t('pages.inbounds.form.echDynamicTitle')}
</div>
<Typography.Paragraph type="secondary" style={{ fontSize: 12, marginBottom: 8 }}>
{t('pages.inbounds.form.echDynamicIntro')}
</Typography.Paragraph>
{value.trim() !== '' && !parsed && (
<Typography.Paragraph
type="warning"
data-testid="ech-existing-not-dynamic"
style={{ fontSize: 12, marginBottom: 8 }}
>
{t('pages.inbounds.form.echExistingNotDynamic')}
</Typography.Paragraph>
)}
{/* component={false}: rendered inside the inbound/host <form>, and a nested
<form> element is invalid HTML. */}
<Form layout="vertical" size="small" colon={false} component={false}>
<Form.Item htmlFor={`${uid}-preset`} label={t('pages.inbounds.form.echPresetLabel')}>
<Select
id={`${uid}-preset`}
value={preset}
disabled={disabled}
showSearch={{ optionFilterProp: 'label' }}
data-testid="ech-preset-select"
onChange={pickPreset}
options={[
...pairPresetOptions(t),
{
label: t('pages.inbounds.form.addCustomOption'),
options: [{ value: ADVANCED, label: t('pages.inbounds.form.echPresetAdvanced') }],
},
]}
/>
</Form.Item>
{preset === ADVANCED && (
<>
<Form.Item htmlFor={`${uid}-domain`} label={t('pages.inbounds.form.echDynamicDomain')}>
<Select
id={`${uid}-domain`}
value={domainChoice}
disabled={disabled}
showSearch={{ optionFilterProp: 'label' }}
onChange={pickDomain}
options={[
{ value: AUTO, label: t('pages.inbounds.form.echDomainAuto') },
...echDomainOptions(t),
{ value: CUSTOM, label: t('pages.inbounds.form.addCustomOption') },
]}
/>
</Form.Item>
{domainChoice === CUSTOM && (
<Form.Item>
<Input
aria-label={t('pages.inbounds.form.echDynamicDomain')}
value={domainInput}
disabled={disabled}
placeholder={DEFAULT_ECH_DOMAIN}
onChange={(e) => {
setDomainInput(e.target.value);
setPreview(null);
}}
/>
</Form.Item>
)}
<Form.Item htmlFor={`${uid}-doh`} label={t('pages.inbounds.form.echDynamicDoh')}>
<Select
id={`${uid}-doh`}
value={dohChoice}
disabled={disabled}
showSearch={{ optionFilterProp: 'label' }}
onChange={pickDoh}
options={[
...dohEndpointOptions(t),
{
label: t('pages.inbounds.form.addCustomOption'),
options: [{ value: CUSTOM, label: t('pages.inbounds.form.echDynamicCustom') }],
},
]}
/>
</Form.Item>
{dohChoice === CUSTOM && (
<Form.Item>
<Input
aria-label={t('pages.inbounds.form.echDynamicDoh')}
value={dohInput}
disabled={disabled}
placeholder={DEFAULT_DOH_ENDPOINT}
status={dohInput && !isSupportedDohUrl(dohInput) ? 'error' : undefined}
onChange={(e) => {
setDohInput(e.target.value);
setPreview(null);
}}
/>
</Form.Item>
)}
{dohChoice === CUSTOM && dohInput && !isSupportedDohUrl(dohInput) && (
<Typography.Paragraph type="danger" style={{ fontSize: 12, marginBottom: 8 }}>
{t('pages.inbounds.form.echDynamicUnsupportedScheme')}
</Typography.Paragraph>
)}
</>
)}
<Typography.Paragraph type="secondary" style={{ fontSize: 12, marginBottom: 8 }}>
{t('pages.inbounds.form.echMainlandAdvisory')}
</Typography.Paragraph>
</Form>
{built ? (
<Typography.Paragraph
code
copyable={{ text: built }}
data-testid="ech-dynamic-preview"
style={{ fontSize: 12, marginBottom: 8, wordBreak: 'break-all' }}
>
{built}
</Typography.Paragraph>
) : (
<Typography.Paragraph type="danger" style={{ fontSize: 12, marginBottom: 8 }}>
{t('pages.inbounds.form.echDynamicInvalid')}
</Typography.Paragraph>
)}
<Button
type="primary"
size="small"
icon={<ThunderboltOutlined aria-hidden />}
disabled={disabled || !built}
data-testid="ech-dynamic-apply"
onClick={apply}
>
{t('pages.inbounds.form.echDynamicApply')}
</Button>
{preview && (
<>
<Divider style={{ margin: '12px 0' }} />
<Alert
type="warning"
showIcon
data-testid="ech-dynamic-overwrite-warning"
title={t('pages.inbounds.form.echDynamicOverwriteTitle')}
description={
<>
<div>{t('pages.inbounds.form.echDynamicOverwriteBody')}</div>
<div data-testid="ech-dynamic-overwrite-value" style={{ marginTop: 4 }}>
<Typography.Text code>{preview}</Typography.Text>
</div>
</>
}
action={
<Space orientation="vertical">
<Button size="small" type="primary" onClick={confirmOverwrite}>
{t('pages.inbounds.form.echDynamicOverwriteConfirm')}
</Button>
<Button size="small" onClick={() => setPreview(null)}>
{t('cancel')}
</Button>
</Space>
}
/>
</>
)}
</div>
);
}
+181
View File
@@ -0,0 +1,181 @@
import type { TFunction } from 'i18next';
/** Xray splits `echConfigList` on the first '+' into ECH target domain and DoH URL. */
export const ECH_DOH_SEPARATOR = '+';
export const DEFAULT_ECH_DOMAIN = 'cloudflare-ech.com';
export const DEFAULT_DOH_ENDPOINT = 'https://dns.alidns.com/dns-query';
interface DohEndpoint {
value: string;
brand: string;
recommended?: boolean;
}
interface DohGroup {
key: 'china' | 'global' | 'europe' | 'udp';
endpoints: readonly DohEndpoint[];
}
/* A resolver only works here if it publishes HTTPS RRs (carrying `ech=`). The
* udp:// entries are plain DNS; Xray dials them the same way, by prefix. */
export const DOH_ENDPOINT_GROUPS: readonly DohGroup[] = [
{
key: 'china',
endpoints: [
{ value: 'https://dns.alidns.com/dns-query', brand: 'AliDNS', recommended: true },
{ value: 'https://sm2.doh.pub/dns-query', brand: 'DNSPod SM2' },
{ value: 'https://doh.360.cn/dns-query', brand: '360' },
{ value: 'https://doh.onedns.net/dns-query', brand: 'OneDNS' },
],
},
{
key: 'global',
endpoints: [
{ value: 'https://cloudflare-dns.com/dns-query', brand: 'Cloudflare' },
{ value: 'https://1.1.1.1/dns-query', brand: 'Cloudflare IP' },
{ value: 'https://dns.google/dns-query', brand: 'Google' },
{ value: 'https://dns.quad9.net/dns-query', brand: 'Quad9' },
],
},
{
key: 'europe',
endpoints: [
{ value: 'https://doh.applied-privacy.net/query', brand: 'Applied Privacy' },
{ value: 'https://odvr.nic.cz/doh', brand: 'CZ.NIC' },
{ value: 'https://dnsforge.de/dns-query', brand: 'DNSforge' },
{ value: 'https://doh.dns.sb/dns-query', brand: 'DNS.SB' },
],
},
{
key: 'udp',
endpoints: [
{ value: 'udp://208.67.220.220:443', brand: 'OpenDNS' },
{ value: 'udp://149.112.112.112:9953', brand: 'Quad9' },
{ value: 'udp://45.90.28.0:5353', brand: 'NextDNS' },
],
},
] as const;
interface EchDomainPreset {
value: string;
brand: string;
recommended?: boolean;
}
/* Targets that publish HTTPS RRs, plus the brand the operator recognises. */
export const ECH_DOMAIN_PRESETS: readonly EchDomainPreset[] = [
{ value: 'cloudflare-ech.com', brand: 'Cloudflare ECH', recommended: true },
{ value: 'crypto.cloudflare.com', brand: 'Cloudflare Crypto' },
{ value: 'encryptedsni.com', brand: 'Cloudflare Encrypted SNI' },
{ value: 'icook.hk', brand: 'iCook HK' },
{ value: 'cm.edu.kg', brand: 'CM University' },
{ value: 'godotengine.org', brand: 'Godot Engine' },
{ value: 'www.britannica.com', brand: 'Britannica' },
{ value: 'www.prometheus.io', brand: 'Prometheus' },
{ value: 'www.kyocera.com', brand: 'Kyocera' },
{ value: 'celestia.org', brand: 'Celestia' },
{ value: 'lido.fi', brand: 'Lido' },
] as const;
const DOMAIN_RE = /^([a-z0-9-]+\.)+[a-z]{2,}$/i;
/* Xray dials the DoH half by case-sensitive prefix match: https:// and h2c://
* over HTTP, udp:// plain DNS. Anything else never resolves. */
export const SUPPORTED_DOH_SCHEMES = ['https', 'h2c', 'udp'] as const;
function normalizeDohUrl(raw: string): string | null {
const url = raw.trim();
if (!url || /\s/.test(url)) return null;
const match = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(url);
if (!match) return null;
const scheme = match[1].toLowerCase();
if (!(SUPPORTED_DOH_SCHEMES as readonly string[]).includes(scheme)) return null;
try {
if (!new URL(url).hostname) return null;
} catch {
return null;
}
// Lower-case the scheme only: Xray's prefix match rejects "HTTPS://" otherwise.
return `${scheme}://${url.slice(match[0].length)}`;
}
export function formatDynamicEch(domain: string, dohUrl: string): string | null {
const d = domain.trim();
const u = normalizeDohUrl(dohUrl);
if (!DOMAIN_RE.test(d) || d.includes(ECH_DOH_SEPARATOR)) return null;
if (!u) return null;
return `${d}${ECH_DOH_SEPARATOR}${u}`;
}
export function parseDynamicEch(value: string): { domain: string; dohUrl: string } | null {
const raw = value.trim();
const split = raw.indexOf(ECH_DOH_SEPARATOR);
if (split <= 0) return null;
const domain = raw.slice(0, split).trim();
const dohUrl = normalizeDohUrl(raw.slice(split + 1));
if (!DOMAIN_RE.test(domain) || !dohUrl) return null;
return { domain, dohUrl };
}
export function isSupportedDohUrl(url: string): boolean {
return normalizeDohUrl(url) !== null;
}
interface PresetOption {
value: string;
label: string;
}
export function dohEndpointOptions(t: TFunction): { label: string; options: PresetOption[] }[] {
const groupLabels: Record<(typeof DOH_ENDPOINT_GROUPS)[number]['key'], string> = {
china: t('pages.inbounds.form.echDohGroup.china'),
global: t('pages.inbounds.form.echDohGroup.global'),
europe: t('pages.inbounds.form.echDohGroup.europe'),
udp: t('pages.inbounds.form.echDohGroup.udp'),
};
const recommended = t('pages.inbounds.form.echRecommended');
return DOH_ENDPOINT_GROUPS.map((group) => ({
label: groupLabels[group.key],
options: group.endpoints.map((e) => ({
value: e.value,
label:
'recommended' in e && e.recommended
? `${e.brand} — ${recommended} · ${e.value}`
: `${e.brand} · ${e.value}`,
})),
}));
}
export function echDomainOptions(t: TFunction): PresetOption[] {
const recommended = t('pages.inbounds.form.echRecommended');
return ECH_DOMAIN_PRESETS.map((d) => ({
value: d.value,
label:
'recommended' in d && d.recommended
? `${d.value} — ${recommended} · ${d.brand}`
: `${d.value} · ${d.brand}`,
}));
}
export const DEFAULT_ECH_PAIR = `${DEFAULT_ECH_DOMAIN}${ECH_DOH_SEPARATOR}${DEFAULT_DOH_ENDPOINT}`;
/** Ready-made pairs for the single preset dropdown, grouped by target front. */
export function pairPresetOptions(t: TFunction): { label: string; options: PresetOption[] }[] {
const recommended = t('pages.inbounds.form.echRecommended');
const cloudflare = DOH_ENDPOINT_GROUPS.flatMap((g) => g.endpoints).map((e) => ({
value: `${DEFAULT_ECH_DOMAIN}${ECH_DOH_SEPARATOR}${e.value}`,
label:
'recommended' in e && e.recommended
? `${e.brand} · ${e.value} — ${recommended}`
: `${e.brand} · ${e.value}`,
}));
const others = ECH_DOMAIN_PRESETS.filter((d) => d.value !== DEFAULT_ECH_DOMAIN).map((d) => ({
value: `${d.value}${ECH_DOH_SEPARATOR}${DEFAULT_DOH_ENDPOINT}`,
label: `${d.value} · ${d.brand}`,
}));
return [
{ label: t('pages.inbounds.form.echDynamicTitle'), options: cloudflare },
{ label: t('pages.inbounds.form.echPresetOtherDomains'), options: others },
];
}
+41 -3
View File
@@ -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')}
@@ -34,8 +34,9 @@ exports[`inbound security forms > TlsForm field structure is stable 1`] = `
"Digital Certificate",
"Master Key Log",
"ECH Sockopt",
"ECH config list",
"Preset combination",
"ECH key",
"ECH config",
"Pinned Peer Cert SHA-256",
"Verify Peer Cert By Name",
]
@@ -0,0 +1,115 @@
import { describe, expect, it, vi } from 'vitest';
import { fireEvent, screen, within } from '@testing-library/react';
import DynamicEchHelper from '@/components/form/DynamicEchHelper';
import { renderWithProviders } from './test-utils';
function combos() {
const helper = screen.getByTestId('ech-dynamic-helper');
return within(helper).getAllByRole('combobox') as HTMLElement[];
}
/** antd virtualises long option lists, so filter with the search box first. */
function chooseOption(combobox: HTMLElement, search: string, match: string) {
fireEvent.mouseDown(combobox);
fireEvent.change(combobox, { target: { value: search } });
const option = Array.from(
document.querySelectorAll(
'.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option',
),
).find((o) => (o.getAttribute('title') ?? o.textContent ?? '').includes(match));
if (!option) throw new Error(`option not found: ${match}`);
fireEvent.click(option);
}
describe('DynamicEchHelper', () => {
it('defaults to the Cloudflare ECH front with AliDNS', () => {
// Behind Cloudflare the ECH record belongs to Cloudflare, not to the proxied
// hostname, so the preset must not depend on the inbound SNI.
const onApply = vi.fn();
renderWithProviders(<DynamicEchHelper onApply={onApply} />);
expect(screen.getByTestId('ech-dynamic-preview').textContent).toBe(
'cloudflare-ech.com+https://dns.alidns.com/dns-query',
);
fireEvent.click(screen.getByTestId('ech-dynamic-apply'));
expect(onApply).toHaveBeenCalledWith('cloudflare-ech.com+https://dns.alidns.com/dns-query');
});
it('writes the pair chosen from the preset dropdown', () => {
const onApply = vi.fn();
renderWithProviders(<DynamicEchHelper onApply={onApply} />);
chooseOption(combos()[0], 'dns.google', 'https://dns.google/dns-query');
fireEvent.click(screen.getByTestId('ech-dynamic-apply'));
expect(onApply).toHaveBeenCalledWith('cloudflare-ech.com+https://dns.google/dns-query');
});
it('offers the udp:// resolvers from the issue list', () => {
const onApply = vi.fn();
renderWithProviders(<DynamicEchHelper onApply={onApply} />);
chooseOption(combos()[0], 'OpenDNS', 'udp://208.67.220.220:443');
fireEvent.click(screen.getByTestId('ech-dynamic-apply'));
expect(onApply).toHaveBeenCalledWith('cloudflare-ech.com+udp://208.67.220.220:443');
});
it('normalises an upper-case scheme typed into the custom builder', () => {
// Xray prefix-matches case-sensitively, so "HTTPS://" must ship lower-cased.
const onApply = vi.fn();
renderWithProviders(<DynamicEchHelper onApply={onApply} />);
chooseOption(combos()[0], 'Custom combination', 'Custom combination');
const dohSelect = combos()[2];
chooseOption(dohSelect, 'Custom URL', 'Custom URL');
const customInput = screen.getAllByRole('textbox').at(-1) as HTMLInputElement;
fireEvent.change(customInput, { target: { value: 'HTTPS://dns.google/dns-query' } });
fireEvent.click(screen.getByTestId('ech-dynamic-apply'));
expect(onApply).toHaveBeenCalledWith('cloudflare-ech.com+https://dns.google/dns-query');
});
it('refuses a custom resolver URL Xray cannot dial', () => {
renderWithProviders(<DynamicEchHelper onApply={vi.fn()} />);
chooseOption(combos()[0], 'Custom combination', 'Custom combination');
const dohSelect = combos()[2];
chooseOption(dohSelect, 'Custom URL', 'Custom URL');
const customInput = screen.getAllByRole('textbox').at(-1) as HTMLInputElement;
fireEvent.change(customInput, { target: { value: 'tls://1.1.1.1/dns-query' } });
expect(screen.getByText(/https:\/\/, h2c:\/\/ or udp:\/\//)).toBeTruthy();
expect((screen.getByTestId('ech-dynamic-apply') as HTMLButtonElement).disabled).toBe(true);
});
it('warns before replacing a value that is not in the dynamic form', () => {
// A bare DoH URL or a local base64 list is valid for Xray; overwriting it
// silently would destroy a working config.
renderWithProviders(<DynamicEchHelper onApply={vi.fn()} value="AEX+DQBB" />);
expect(screen.getByTestId('ech-existing-not-dynamic')).toBeTruthy();
});
it('shows no such warning when the current value is already dynamic', () => {
renderWithProviders(
<DynamicEchHelper
onApply={vi.fn()}
value="cloudflare-ech.com+https://dns.google/dns-query"
/>,
);
expect(screen.queryByTestId('ech-existing-not-dynamic')).toBeNull();
expect(screen.getByTestId('ech-dynamic-preview').textContent).toBe(
'cloudflare-ech.com+https://dns.google/dns-query',
);
});
it('asks for confirmation before replacing an existing local ECH keypair', () => {
const onApply = vi.fn();
const onClearLocalKeys = vi.fn();
renderWithProviders(
<DynamicEchHelper hasLocalKeys onApply={onApply} onClearLocalKeys={onClearLocalKeys} />,
);
fireEvent.click(screen.getByTestId('ech-dynamic-apply'));
expect(screen.getByTestId('ech-dynamic-overwrite-warning')).toBeTruthy();
expect(onApply).not.toHaveBeenCalled();
fireEvent.click(screen.getByText('Replace and clear local key'));
expect(onApply).toHaveBeenCalledWith('cloudflare-ech.com+https://dns.alidns.com/dns-query');
expect(onClearLocalKeys).toHaveBeenCalled();
});
});
@@ -0,0 +1,138 @@
import { describe, expect, it } from 'vitest';
import { Form } from 'antd';
import type { ReactNode } from 'react';
import { FormProvider, useForm, useFormContext, useWatch } from 'react-hook-form';
import { fireEvent, screen, waitFor, within } from '@testing-library/react';
import { TlsForm } from '@/pages/inbounds/form/security';
import type { InboundFormValues } from '@/schemas/forms/inbound-form';
import { renderWithProviders } from './test-utils';
function EchProbe() {
const { control } = useFormContext();
const ech = useWatch({ control, name: 'streamSettings.tlsSettings.settings.echConfigList' });
const keys = useWatch({ control, name: 'streamSettings.tlsSettings.echServerKeys' });
return (
<>
<output data-testid="probe-ech">{typeof ech === 'string' ? ech : ''}</output>
<output data-testid="probe-keys">{typeof keys === 'string' ? keys : ''}</output>
</>
);
}
function FormHarness({
children,
defaultValues = {},
}: {
children: ReactNode;
defaultValues?: Record<string, unknown>;
}) {
const methods = useForm<InboundFormValues>({ defaultValues: defaultValues as never });
return (
<FormProvider {...methods}>
<Form>
<EchProbe />
{children}
</Form>
</FormProvider>
);
}
const noop = () => {};
function renderTls(defaultValues?: Record<string, unknown>) {
return renderWithProviders(
<FormHarness defaultValues={defaultValues}>
<TlsForm
saving={false}
setCertFromPanel={noop}
clearCertFiles={noop}
pinFromCert={noop}
pinFromRemote={noop}
getNewEchCert={noop}
clearEchCert={noop}
/>
</FormHarness>,
);
}
/** The generator is inline now, so it is present as soon as the TLS form renders. */
async function openHelper() {
return screen.findByTestId('ech-dynamic-helper');
}
describe('inbound TLS dynamic ECH helper', () => {
it('applies the default Cloudflare + AliDNS value into echConfigList', async () => {
renderTls();
const helper = await openHelper();
expect(within(helper).getByTestId('ech-dynamic-preview').textContent).toBe(
'cloudflare-ech.com+https://dns.alidns.com/dns-query',
);
fireEvent.click(within(helper).getByTestId('ech-dynamic-apply'));
await waitFor(() =>
expect(screen.getByTestId('probe-ech').textContent).toBe(
'cloudflare-ech.com+https://dns.alidns.com/dns-query',
),
);
});
it('asks for confirmation before replacing an existing local ECH keypair', async () => {
// The CDN-invalid local key is exactly what this feature exists to prevent
// shipping, so overwriting it silently would hide the mistake.
renderTls({
streamSettings: {
tlsSettings: {
echServerKeys: 'LOCAL-PRIVATE-KEY',
settings: { echConfigList: '' },
},
},
});
const helper = await openHelper();
fireEvent.click(within(helper).getByTestId('ech-dynamic-apply'));
expect(await within(helper).findByTestId('ech-dynamic-overwrite-warning')).toBeTruthy();
expect(screen.getByTestId('probe-ech').textContent).toBe('');
expect(screen.getByTestId('probe-keys').textContent).toBe('LOCAL-PRIVATE-KEY');
});
it('clears the dead local key once the operator confirms', async () => {
renderTls({
streamSettings: {
tlsSettings: {
echServerKeys: 'LOCAL-PRIVATE-KEY',
settings: { echConfigList: '' },
},
},
});
const helper = await openHelper();
fireEvent.click(within(helper).getByTestId('ech-dynamic-apply'));
fireEvent.click(await within(helper).findByText('Replace and clear local key'));
await waitFor(() =>
expect(screen.getByTestId('probe-ech').textContent).toBe(
'cloudflare-ech.com+https://dns.alidns.com/dns-query',
),
);
expect(screen.getByTestId('probe-keys').textContent).toBe('');
});
it('recovers an existing dynamic value instead of starting from defaults', async () => {
renderTls({
streamSettings: {
tlsSettings: {
settings: { echConfigList: 'crypto.cloudflare.com+https://dns.google/dns-query' },
},
},
});
const helper = await openHelper();
expect(within(helper).getByTestId('ech-dynamic-preview').textContent).toBe(
'crypto.cloudflare.com+https://dns.google/dns-query',
);
});
it('warns that local keys do not survive a CDN proxy', () => {
renderTls();
expect(screen.getByTestId('ech-cdn-warning').textContent).toMatch(/Cloudflare CDN/i);
});
});
+152
View File
@@ -0,0 +1,152 @@
import { describe, expect, it } from 'vitest';
import type { TFunction } from 'i18next';
import {
DEFAULT_DOH_ENDPOINT,
DEFAULT_ECH_DOMAIN,
DEFAULT_ECH_PAIR,
DOH_ENDPOINT_GROUPS,
ECH_DOMAIN_PRESETS,
ECH_DOH_SEPARATOR,
formatDynamicEch,
isSupportedDohUrl,
pairPresetOptions,
parseDynamicEch,
} from '@/lib/xray/ech-presets';
function optionValues(groups: typeof DOH_ENDPOINT_GROUPS): string[] {
return groups.flatMap((g) => g.endpoints.map((o) => o.value));
}
const t = ((key: string) => key) as unknown as TFunction;
describe('dynamic ECH presets', () => {
it('every shipped DoH preset is one Xray can actually resolve', () => {
// A preset with any other scheme passes review but never resolves, because
// Xray matches the scheme by literal prefix before dialing.
const bad = optionValues(DOH_ENDPOINT_GROUPS).filter((v) => !isSupportedDohUrl(v));
expect(bad).toEqual([]);
});
it('every ECH target domain is a bare registrable name, not a URL', () => {
// Xray splits the string on the first '+', so a scheme in the domain half
// would be sent to the DNS resolver verbatim and never resolve.
const bad = ECH_DOMAIN_PRESETS.filter((d) => !/^([a-z0-9-]+\.)+[a-z]{2,}$/.test(d.value));
expect(bad.map((d) => d.value)).toEqual([]);
});
it('every preset dropdown pair round-trips through the Xray parser', () => {
// The dropdown is a single click away from echConfigList; a pair the parser
// rejects here would ship a value every client fails to resolve.
const pairs = pairPresetOptions(t).flatMap((g) => g.options.map((o) => o.value));
expect(pairs.length).toBeGreaterThan(0);
expect(pairs.filter((p) => parseDynamicEch(p) === null)).toEqual([]);
expect(pairs).toContain(DEFAULT_ECH_PAIR);
});
it('offers every resolver under the Cloudflare front, udp ones included', () => {
const [cloudflare] = pairPresetOptions(t);
expect(cloudflare.options).toHaveLength(optionValues(DOH_ENDPOINT_GROUPS).length);
expect(cloudflare.options.some((o) => o.value.includes('udp://'))).toBe(true);
});
it.each(['https', 'h2c', 'udp'])('builds a value for the %s scheme Xray supports', (scheme) => {
const url = `${scheme}://dns.example.com/dns-query`;
expect(formatDynamicEch('cloudflare-ech.com', url)).toBe(`cloudflare-ech.com+${url}`);
});
it.each(['http', 'quic', 'tcp', 'tls', 'doq', 'ftp'])(
'rejects the %s scheme at every entry point',
(scheme) => {
// Accepting these would ship a config that fails at the client, where the
// operator cannot see the panel's advice any more.
const url = `${scheme}://1.1.1.1/dns-query`;
expect(isSupportedDohUrl(url)).toBe(false);
expect(formatDynamicEch('cloudflare-ech.com', url)).toBeNull();
expect(parseDynamicEch(`cloudflare-ech.com+${url}`)).toBeNull();
},
);
it('preselects Cloudflare over AliDNS, the mainland-China reachable pair', () => {
expect(formatDynamicEch(DEFAULT_ECH_DOMAIN, DEFAULT_DOH_ENDPOINT)).toBe(
'cloudflare-ech.com+https://dns.alidns.com/dns-query',
);
});
it('trims pasted whitespace instead of emitting an unresolvable string', () => {
expect(formatDynamicEch(' cloudflare-ech.com ', ' https://dns.google/dns-query ')).toBe(
'cloudflare-ech.com+https://dns.google/dns-query',
);
});
it('rejects a domain that already embeds a DoH URL', () => {
// Pasting a full existing value into the domain box would otherwise produce
// "...+https://a+https://b", which no client can parse.
expect(
formatDynamicEch(
'cloudflare-ech.com+https://dns.alidns.com/dns-query',
'https://dns.google/dns-query',
),
).toBeNull();
});
it('rejects a DoH half that is not a URI', () => {
expect(formatDynamicEch('cloudflare-ech.com', 'dns.google')).toBeNull();
expect(formatDynamicEch('cloudflare-ech.com', '')).toBeNull();
expect(formatDynamicEch('', 'https://dns.google/dns-query')).toBeNull();
});
it('splits an existing dynamic value back into its two halves', () => {
expect(
parseDynamicEch(`cloudflare-ech.com${ECH_DOH_SEPARATOR}https://dns.google/dns-query`),
).toEqual({ domain: 'cloudflare-ech.com', dohUrl: 'https://dns.google/dns-query' });
});
it('refuses to split a value whose DoH half uses an unsupported scheme', () => {
// Otherwise pasting a quic:// value back into the helper would round-trip an
// endpoint Xray can never reach.
expect(parseDynamicEch('cloudflare-ech.com+quic://1.1.1.1/dns-query')).toBeNull();
});
it('does not mistake a local ECHConfigList (base64) for a dynamic value', () => {
// "Get New ECH Cert" writes a base64 blob; the helper must not try to
// reverse-engineer it.
expect(parseDynamicEch('AEX+DQBB')).toBeNull();
expect(parseDynamicEch('')).toBeNull();
});
it('lower-cases a pasted scheme so Xray prefix-matches it', () => {
// Xray compares with strings.HasPrefix(server, "https://") — case-sensitive.
// Passing "HTTPS://" through unchanged ships a config every client fails on.
expect(formatDynamicEch('cloudflare-ech.com', 'HTTPS://dns.google/dns-query')).toBe(
'cloudflare-ech.com+https://dns.google/dns-query',
);
expect(formatDynamicEch('cloudflare-ech.com', 'UDP://1.1.1.1')).toBe(
'cloudflare-ech.com+udp://1.1.1.1',
);
expect(isSupportedDohUrl('H2C://dns.google/dns-query')).toBe(true);
});
it('rejects a resolver URL carrying whitespace Xray would put on the wire', () => {
// The old prefix-only regex accepted these and the junk went into the value.
expect(isSupportedDohUrl('https://dns.google/dns-query extra')).toBe(false);
expect(isSupportedDohUrl('https://dns.google /dns-query')).toBe(false);
expect(formatDynamicEch('cloudflare-ech.com', 'https://dns.google/dns-query extra')).toBeNull();
});
it('rejects a resolver URL with no host', () => {
// new URL("https://?x") parses but carries an empty hostname: Xray would
// dial nothing and fail the handshake.
expect(isSupportedDohUrl('https://?x')).toBe(false);
expect(formatDynamicEch('cloudflare-ech.com', 'https://?x')).toBeNull();
});
it('defaults the target to Cloudflare, not the inbound SNI', () => {
// Behind Cloudflare the ECH record belongs to Cloudflare's ECH front, so the
// helper must not derive it from the proxied hostname.
expect(DEFAULT_ECH_DOMAIN).toBe('cloudflare-ech.com');
expect(formatDynamicEch(DEFAULT_ECH_DOMAIN, DEFAULT_DOH_ENDPOINT)).toBe(
'cloudflare-ech.com+https://dns.alidns.com/dns-query',
);
});
});
+30 -2
View File
@@ -611,7 +611,6 @@
"usageOption": "خيار الاستخدام",
"buildChain": "بناء السلسلة",
"echKey": "ECH key",
"echConfig": "تكوين ECH",
"pinnedPeerCertSha256": "SHA-256 لشهادة النظير المثبَّتة",
"pinnedPeerCertSha256Tip": "تجزئات SHA-256 لشهادة النظير كسلسلة سداسية عشرية (مثل e8e2d3…)، مفصولة بفواصل. للوحة فقط — لا تُكتب في إعدادات xray على الخادم، لكنها تُضمَّن في روابط المشاركة ليتمكَّن العملاء من تثبيت الشهادة.",
"pinnedPeerCertSha256Placeholder": "تجزئة (تجزئات) سداسية عشرية، مفصولة بفواصل",
@@ -679,6 +678,36 @@
},
"echSockopt": "ECH Sockopt",
"echSockoptTip": "خيارات السوكيت للاتصال اللي Xray بتستخدمه عشان تجيب قائمة إعدادات ECH (مثلاً توجيه الاستعلام عبر صادر dialerProxy). سيبه متعطّل عشان تستخدم الإعدادات الافتراضية.",
"echCdnWarningTitle": "مفاتيح ECH المتولّدة محليًا بتشتغل بس مع الاتصال المباشر",
"echCdnWarningBody": "زوج المفاتيح اللي بيتعمل بـ«Get New ECH Cert» بيبقى صالح بس لما العملاء يتصلوا بالـ VPS ده مباشرة. لو الوارد محطوط ورا Cloudflare CDN (يعني VLESS + xhttp/WS ورا السحابة البرتقالية)، فـ Cloudflare بتنهي TLS الخارجي بمفاتيح ECH بتاعتها وبتغيّرها باستمرار، فالمفتاح المحلي هيفشل في المصافحة على طول. استخدم مساعد ECH الديناميكي بدل كده.",
"echDynamicTitle": "ECH ديناميكي عبر Cloudflare (بيشتغل ورا السحابة البرتقالية)",
"echPresetLabel": "تركيبة جاهزة",
"echPresetOtherDomains": "نطاقات ECH مستهدفة تانية",
"echPresetAdvanced": "تركيبة مخصصة…",
"echDynamicIntro": "بيملأ قائمة إعدادات ECH ببحث ديناميكي من ناحية العميل (النطاق المستهدف + مُحلِّل DoH). العميل بيسأل المُحلِّل عن سجل HTTPS بتاع النطاق في كل اتصال، فبيشوف دايمًا مفاتيح ECH الحالية — بما فيها مفاتيح Cloudflare المتغيّرة.",
"echDynamicDomain": "النطاق المستهدف لـ ECH",
"echDynamicDoh": "مُحلِّل DoH",
"echDomainAuto": "تلقائي — احل سجل ECH للـ SNI بتاع العقدة",
"echDynamicApply": "طبّق على قائمة إعدادات ECH",
"echDynamicCustom": "URL مخصّص…",
"echDynamicInvalid": "أدخل نطاقًا صالحًا وURL لمُحلِّل (https أو h2c أو udp) عشان تبني القيمة.",
"echDynamicUnsupportedScheme": "Xray بيدعم بس https:// أو h2c:// أو udp:// لحل إعدادات ECH، فالعنوان ده مش هيرد أبدًا.",
"echDynamicPlaceholder": "cloudflare-ech.com+https://dns.alidns.com/dns-query",
"echDynamicOverwriteTitle": "الوارد ده فيه زوج مفاتيح ECH متولّد محليًا",
"echDynamicOverwriteBody": "ورا CDN، زوج المفاتيح ده مش بيتستخدم أبدًا: الـ CDN هي اللي بتنهي مصافحة TLS. تطبيق القيمة الديناميكية هيعوّض قائمة إعدادات ECH ويمسح مفتاح ECH المحلي.",
"echDynamicOverwriteConfirm": "عوّض وامسح المفتاح المحلي",
"echExistingNotDynamic": "قائمة إعدادات ECH دي مش بصيغة ديناميكية؛ لو مليتها هتعوّض القيم اللي ظبطتها بإيدك.",
"echDohGroup": {
"china": "الصين القارية (متاحة من ورا الـ GFW)",
"global": "عالمي",
"europe": "أوروبا",
"udp": "DNS عادي (udp://)"
},
"echRecommended": "موصى به",
"echMainlandAdvisory": "الـ DNS المحلي بيتطلب نطاق مستهدف سجل HTTPS بتاعه فيه ECH؛ اتأكد إن الاتنين شغالين قبل الحفظ.",
"echConfigList": "قائمة إعدادات ECH",
"echConfigListHint": "بتتبعت للعملاء في روابط المشاركة والاشتراكات. بتقبل ECHConfigList محلي (من الزرار اللي تحت) أو الصيغة الديناميكية domain+DoH URL.",
"echKeyHint": "للاتصال المباشر بس — سيبه فاضي لو الوارد محطوط ورا CDN.",
"curvePreferences": "تفضيلات المنحنى",
"curvePreferencesTip": "بتقصر منحنيات تبادل مفاتيح TLS اللي السيرفر بيعرضها، بترتيب الأفضلية (مثلاً X25519MLKEM768, X25519). سيبه فاضي عشان تستخدم الإعدادات الافتراضية لـ Xray-core.",
"masterKeyLog": "سجل المفتاح الرئيسي",
@@ -2360,7 +2389,6 @@
"fingerprint": "بصمة",
"pins": "SHA-256 للشهادة المثبّتة",
"allowInsecure": "السماح بغير الآمن",
"echConfigList": "قائمة إعدادات ECH",
"muxParams": "Mux",
"sockoptParams": "Sockopt",
"finalMask": "Final Mask",
+30 -2
View File
@@ -613,9 +613,38 @@
"usageOption": "Usage Option",
"buildChain": "Build Chain",
"echKey": "ECH key",
"echConfig": "ECH config",
"echSockopt": "ECH Sockopt",
"echSockoptTip": "Socket options for the connection Xray uses to fetch the ECH config list (e.g. route the lookup through a dialerProxy outbound). Leave disabled to use defaults.",
"echCdnWarningTitle": "Locally generated ECH keys only work for direct connections",
"echCdnWarningBody": "A keypair made by \"Get New ECH Cert\" is only valid when clients connect straight to this VPS. If the inbound is proxied through Cloudflare CDN (VLESS + xhttp/WS behind the orange cloud), Cloudflare terminates the outer TLS with its own ECH keys and rotates them, so a local key always fails the handshake. Use the Dynamic ECH helper instead.",
"echDynamicTitle": "Cloudflare dynamic ECH (works behind the orange cloud)",
"echPresetLabel": "Preset combination",
"echPresetOtherDomains": "Other ECH target domains",
"echPresetAdvanced": "Custom combination…",
"echDynamicIntro": "Fills the ECH config list with a client-side dynamic lookup (target domain + DoH resolver). The client asks the resolver for the domain's HTTPS record on every connection, so it always sees the current ECH keys — including Cloudflare's rotated ones.",
"echDynamicDomain": "ECH target domain",
"echDynamicDoh": "DoH resolver",
"echDomainAuto": "Automatic — resolve the ECH record for the node's SNI",
"echDynamicApply": "Apply to ECH config list",
"echDynamicCustom": "Custom URL…",
"echDynamicInvalid": "Enter a valid domain and a resolver URL (https, h2c or udp) to build the value.",
"echDynamicUnsupportedScheme": "Xray only resolves the ECH config over https://, h2c:// or udp://. This endpoint will never answer.",
"echDynamicPlaceholder": "cloudflare-ech.com+https://dns.alidns.com/dns-query",
"echDynamicOverwriteTitle": "This inbound has a locally generated ECH keypair",
"echDynamicOverwriteBody": "Behind a CDN that keypair is never used: the CDN terminates the TLS handshake. Applying the dynamic value will replace the ECH config list and clear the local ECH key.",
"echDynamicOverwriteConfirm": "Replace and clear local key",
"echExistingNotDynamic": "This ECH config list is not in the dynamic form; filling it will replace what you set by hand.",
"echDohGroup": {
"china": "Mainland China (reachable from behind the GFW)",
"global": "Global",
"europe": "Europe",
"udp": "Plain DNS (udp://)"
},
"echRecommended": "recommended",
"echMainlandAdvisory": "Mainland resolvers need a target domain whose HTTPS record carries ECH; verify the pair before saving.",
"echConfigList": "ECH config list",
"echConfigListHint": "Sent to clients in share links and subscriptions. Accepts a local ECHConfigList (from the button below) or the dynamic form domain+DoH URL.",
"echKeyHint": "Only for direct connections — leave empty when the inbound sits behind a CDN.",
"curvePreferences": "Curve Preferences",
"curvePreferencesTip": "Restrict the TLS key-exchange curves the server offers, in preference order (e.g. X25519MLKEM768, X25519). Leave empty to use Xray-core defaults.",
"masterKeyLog": "Master Key Log",
@@ -1098,7 +1127,6 @@
"pins": "Pinned cert SHA-256",
"verifyPeerCertByName": "Verify peer cert by name",
"allowInsecure": "Allow insecure",
"echConfigList": "ECH config list",
"muxParams": "Mux",
"sockoptParams": "Sockopt",
"finalMask": "Final Mask",
+30 -2
View File
@@ -611,13 +611,42 @@
"usageOption": "Opción de uso",
"buildChain": "Construir cadena",
"echKey": "ECH key",
"echConfig": "Config ECH",
"pinnedPeerCertSha256": "SHA-256 del cert. del par fijado",
"pinnedPeerCertSha256Tip": "Hashes SHA-256 del certificado del par como cadena hexadecimal (p. ej. e8e2d3…), separados por comas. Solo en el panel — no se escribe en la config xray del servidor, pero se incluye en los enlaces para que los clientes puedan fijar el certificado.",
"pinnedPeerCertSha256Placeholder": "hash(es) hexadecimal, separados por comas",
"getNewEchCert": "Obtener nuevo cert ECH",
"echSockopt": "ECH Sockopt",
"echSockoptTip": "Opciones de socket para la conexión que Xray usa al obtener la lista de configuración ECH (por ejemplo, enrutar la consulta a través de una salida dialerProxy). Déjalo deshabilitado para usar los valores por defecto.",
"echCdnWarningTitle": "Las claves ECH generadas localmente solo funcionan con conexiones directas",
"echCdnWarningBody": "Un par de claves creado con «Get New ECH Cert» solo es válido cuando los clientes se conectan directamente a este VPS. Si el inbound está en proxy a través de Cloudflare CDN (VLESS + xhttp/WS detrás de la nube naranja), Cloudflare termina el TLS exterior con sus propias claves ECH y las rota, así que una clave local siempre falla el handshake. Usa en su lugar el asistente de ECH dinámico.",
"echDynamicTitle": "ECH dinámico de Cloudflare (funciona detrás de la nube naranja)",
"echPresetLabel": "Combinación predefinida",
"echPresetOtherDomains": "Otros dominios de destino ECH",
"echPresetAdvanced": "Combinación personalizada…",
"echDynamicIntro": "Rellena la lista de configuración ECH con una consulta dinámica del lado del cliente (dominio de destino + resolutor DoH). El cliente pregunta al resolutor por el registro HTTPS del dominio en cada conexión, así que siempre ve las claves ECH actuales, incluidas las rotadas de Cloudflare.",
"echDynamicDomain": "Dominio de destino ECH",
"echDynamicDoh": "Resolutor DoH",
"echDomainAuto": "Automático — resolver el registro ECH del SNI del nodo",
"echDynamicApply": "Aplicar a la lista de configuración ECH",
"echDynamicCustom": "URL personalizada…",
"echDynamicInvalid": "Introduce un dominio válido y una URL de resolutor (https, h2c o udp) para construir el valor.",
"echDynamicUnsupportedScheme": "Xray solo resuelve la configuración ECH mediante https://, h2c:// o udp://. Este destino nunca responderá.",
"echDynamicPlaceholder": "cloudflare-ech.com+https://dns.alidns.com/dns-query",
"echDynamicOverwriteTitle": "Este inbound tiene un par de claves ECH generado localmente",
"echDynamicOverwriteBody": "Detrás de un CDN ese par de claves nunca se usa: el CDN termina el handshake TLS. Aplicar el valor dinámico reemplazará la lista de configuración ECH y borrará la clave ECH local.",
"echDynamicOverwriteConfirm": "Reemplazar y borrar la clave local",
"echExistingNotDynamic": "Esta lista de configuración ECH no está en la forma dinámica; rellenarla sustituirá lo que configuraste a mano.",
"echDohGroup": {
"china": "China continental (accesible detrás del GFW)",
"global": "Mundial",
"europe": "Europa",
"udp": "DNS sin cifrar (udp://)"
},
"echRecommended": "recomendado",
"echMainlandAdvisory": "Los resolutores de China continental necesitan un dominio de destino cuyo registro HTTPS lleve ECH; verifica la combinación antes de guardar.",
"echConfigList": "Lista de configuración ECH",
"echConfigListHint": "Se envía a los clientes en los enlaces de compartir y en las suscripciones. Acepta un ECHConfigList local (del botón de abajo) o la forma dinámica dominio+URL de DoH.",
"echKeyHint": "Solo para conexiones directas — déjalo vacío cuando el inbound esté detrás de un CDN.",
"curvePreferences": "Preferencias de curvas",
"curvePreferencesTip": "Restringe las curvas de intercambio de claves TLS que ofrece el servidor, por orden de preferencia (por ejemplo, X25519MLKEM768, X25519). Déjalo vacío para usar los valores por defecto de Xray-core.",
"masterKeyLog": "Registro de clave maestra",
@@ -2361,7 +2390,6 @@
"pins": "SHA-256 del cert. fijado",
"verifyPeerCertByName": "Verificar cert. del par por nombre",
"allowInsecure": "Permitir inseguro",
"echConfigList": "Lista de config. ECH",
"muxParams": "Mux",
"sockoptParams": "Sockopt",
"finalMask": "Máscara final",
+30 -2
View File
@@ -611,9 +611,38 @@
"usageOption": "گزینه استفاده",
"buildChain": "ساخت زنجیره",
"echKey": "کلید ECH",
"echConfig": "پیکربندی ECH",
"echSockopt": "Sockopt مربوط به ECH",
"echSockoptTip": "تنظیمات سوکت برای اتصالی که Xray برای دریافت لیست پیکربندی ECH استفاده می‌کند (مثلاً عبور این درخواست از یک dialerProxy). برای استفاده از پیش‌فرض غیرفعال بگذارید.",
"echCdnWarningTitle": "کلیدهای ECH که به‌صورت محلی ساخته می‌شوند فقط برای اتصال مستقیم کار می‌کنند",
"echCdnWarningBody": "جفت‌کلیدی که با «Get New ECH Cert» ساخته می‌شود فقط وقتی معتبر است که کارخواهان مستقیم به همین VPS متصل شوند. اگر این inbound از طریق Cloudflare CDN پروکسی شود (یعنی VLESS + xhttp/WS پشت ابر نارنجی)، Cloudflare با کلیدهای ECH خودش TLS بیرونی را پایان می‌دهد و آن‌ها را می‌چرخاند، بنابراین کلید محلی همیشه در دست‌دهی شکست می‌خورد. به‌جایش از راهنمای ECH پویا استفاده کنید.",
"echDynamicTitle": "ECH پویای Cloudflare (پشت ابر نارنجی هم کار می‌کند)",
"echPresetLabel": "ترکیب آماده",
"echPresetOtherDomains": "دامنه‌های هدف دیگر ECH",
"echPresetAdvanced": "ترکیب سفارشی…",
"echDynamicIntro": "لیست پیکربندی ECH را با یک جست‌وجوی پویا از سمت کارخواه پر می‌کند (دامنهٔ هدف + حل‌کنندهٔ DoH). کارخواه در هر اتصال رکورد HTTPS دامنه را از حل‌کننده می‌پرسد، پس همیشه کلیدهای ECH فعلی را می‌بیند — از جمله کلیدهای چرخیدهٔ Cloudflare.",
"echDynamicDomain": "دامنهٔ هدف ECH",
"echDynamicDoh": "حل‌کنندهٔ DoH",
"echDomainAuto": "خودکار — رکورد ECH را برای SNI گره حل کن",
"echDynamicApply": "اعمال روی لیست پیکربندی ECH",
"echDynamicCustom": "URL سفارشی…",
"echDynamicInvalid": "برای ساختن مقدار، یک دامنهٔ معتبر و یک URL حل‌کننده (https، h2c یا udp) وارد کنید.",
"echDynamicUnsupportedScheme": "Xray پیکربندی ECH را فقط با https://، h2c:// یا udp:// حل می‌کند؛ این نقطهٔ پایانی هرگز پاسخ نمی‌دهد.",
"echDynamicPlaceholder": "cloudflare-ech.com+https://dns.alidns.com/dns-query",
"echDynamicOverwriteTitle": "این inbound یک جفت‌کلید ECH ساخته‌شده به‌صورت محلی دارد",
"echDynamicOverwriteBody": "پشت CDN آن جفت‌کلید هرگز استفاده نمی‌شود؛ زیرا CDN دست‌دهی TLS را پایان می‌دهد. اعمال مقدار پویا لیست پیکربندی ECH را جایگزین می‌کند و کلید ECH محلی را پاک می‌کند.",
"echDynamicOverwriteConfirm": "جایگزین کن و کلید محلی را پاک کن",
"echExistingNotDynamic": "این لیست پیکربندی ECH به شکل پویا نیست؛ پر کردن آن چیزی را که دستی تنظیم کرده‌اید جایگزین می‌کند.",
"echDohGroup": {
"china": "سرزمین اصلی چین (قابل دسترسی از پشت GFW)",
"global": "جهانی",
"europe": "اروپا",
"udp": "DNS ساده (udp://)"
},
"echRecommended": "پیشنهادی",
"echMainlandAdvisory": "حل‌کننده‌های داخل چین به دامنهٔ هدفی نیاز دارند که رکورد HTTPS آن ECH داشته باشد؛ پیش از ذخیره، جفت را خودتان بررسی کنید.",
"echConfigList": "لیست پیکربندی ECH",
"echConfigListHint": "در لینک‌های اشتراک و سابسکریپشن‌ها برای کارخواهان فرستاده می‌شود. یک ECHConfigList محلی (از دکمهٔ زیر) یا شکل پویای domain+DoH URL را می‌پذیرد.",
"echKeyHint": "فقط برای اتصال مستقیم — اگر inbound پشت CDN است خالی بگذارید.",
"curvePreferences": "ترجیح منحنی‌ها",
"curvePreferencesTip": "منحنی‌های تبادل کلید TLS که سرور ارائه می‌دهد را به‌ترتیب اولویت محدود می‌کند (مثلاً X25519MLKEM768، X25519). خالی بگذارید تا پیش‌فرض Xray استفاده شود.",
"masterKeyLog": "لاگ کلید اصلی",
@@ -2361,7 +2390,6 @@
"pins": "SHA-256 پین‌شدهٔ گواهی",
"verifyPeerCertByName": "بررسی گواهی همتا بر اساس نام",
"allowInsecure": "اجازهٔ اتصال ناامن",
"echConfigList": "فهرست پیکربندی ECH",
"muxParams": "Mux",
"sockoptParams": "Sockopt",
"finalMask": "ماسک نهایی",
+30 -2
View File
@@ -611,7 +611,6 @@
"usageOption": "Opsi penggunaan",
"buildChain": "Bangun rantai",
"echKey": "ECH key",
"echConfig": "Konfig ECH",
"pinnedPeerCertSha256": "SHA-256 Sertifikat Peer Tersemat",
"pinnedPeerCertSha256Tip": "Hash SHA-256 dari sertifikat peer sebagai string heksadesimal (mis. e8e2d3…), dipisah koma. Hanya panel — tidak ditulis ke konfig xray server, tetapi disertakan dalam link berbagi agar klien dapat menyematkan sertifikat.",
"pinnedPeerCertSha256Placeholder": "hash heksadesimal, dipisah koma",
@@ -679,6 +678,36 @@
},
"echSockopt": "ECH Sockopt",
"echSockoptTip": "Opsi socket untuk koneksi yang dipakai Xray guna mengambil daftar konfig ECH (mis. mengarahkan pencarian melalui outbound dialerProxy). Biarkan nonaktif untuk memakai bawaan.",
"echCdnWarningTitle": "Kunci ECH yang dibuat secara lokal hanya berfungsi untuk koneksi langsung",
"echCdnWarningBody": "Pasangan kunci yang dibuat dengan «Get New ECH Cert» hanya valid saat klien terhubung langsung ke VPS ini. Jika inbound diproksikan melalui Cloudflare CDN (VLESS + xhttp/WS di balik awan oranye), Cloudflare mengakhiri TLS luar dengan kunci ECH miliknya sendiri dan merotasinya, sehingga kunci lokal selalu gagal handshake. Gunakan pembantu ECH dinamis sebagai gantinya.",
"echDynamicTitle": "ECH dinamis Cloudflare (berfungsi di balik awan oranye)",
"echPresetLabel": "Kombinasi preset",
"echPresetOtherDomains": "Domain target ECH lainnya",
"echPresetAdvanced": "Kombinasi khusus…",
"echDynamicIntro": "Mengisi daftar konfig ECH dengan pencarian dinamis di sisi klien (domain target + resolver DoH). Klien menanyakan rekaman HTTPS domain tersebut ke resolver pada setiap koneksi, jadi selalu melihat kunci ECH terkini — termasuk kunci Cloudflare yang sudah dirotasi.",
"echDynamicDomain": "Domain target ECH",
"echDynamicDoh": "Resolver DoH",
"echDomainAuto": "Otomatis — resolve rekaman ECH untuk SNI node",
"echDynamicApply": "Terapkan ke daftar konfig ECH",
"echDynamicCustom": "URL kustom…",
"echDynamicInvalid": "Masukkan domain yang valid dan URL resolver (https, h2c, atau udp) untuk menyusun nilai.",
"echDynamicUnsupportedScheme": "Xray hanya me-resolve konfigurasi ECH lewat https://, h2c://, atau udp://. Titik akhir ini tidak akan pernah menjawab.",
"echDynamicPlaceholder": "cloudflare-ech.com+https://dns.alidns.com/dns-query",
"echDynamicOverwriteTitle": "Inbound ini memiliki pasangan kunci ECH yang dibuat secara lokal",
"echDynamicOverwriteBody": "Di balik CDN, pasangan kunci itu tidak pernah dipakai: CDN-lah yang mengakhiri handshake TLS. Menerapkan nilai dinamis akan mengganti daftar konfig ECH dan menghapus kunci ECH lokal.",
"echDynamicOverwriteConfirm": "Ganti dan hapus kunci lokal",
"echExistingNotDynamic": "Daftar konfig ECH ini tidak dalam bentuk dinamis; mengisinya akan mengganti apa yang Anda atur secara manual.",
"echDohGroup": {
"china": "Tiongkok daratan (dapat diakses dari balik GFW)",
"global": "Mancanegara",
"europe": "Eropa",
"udp": "DNS biasa (udp://)"
},
"echRecommended": "disarankan",
"echMainlandAdvisory": "Resolver di Tiongkok daratan memerlukan domain target yang rekaman HTTPS-nya membawa ECH; verifikasi pasangannya sebelum menyimpan.",
"echConfigList": "Daftar konfig ECH",
"echConfigListHint": "Dikirim ke klien dalam tautan bagikan dan langganan. Menerima ECHConfigList lokal (dari tombol di bawah) atau bentuk dinamis domain+DoH URL.",
"echKeyHint": "Hanya untuk koneksi langsung — biarkan kosong saat inbound berada di balik CDN.",
"curvePreferences": "Preferensi kurva",
"curvePreferencesTip": "Batasi kurva pertukaran kunci TLS yang ditawarkan server, sesuai urutan preferensi (mis. X25519MLKEM768, X25519). Biarkan kosong untuk memakai bawaan Xray-core.",
"masterKeyLog": "Log master key",
@@ -2360,7 +2389,6 @@
"fingerprint": "Fingerprint",
"pins": "SHA-256 sertifikat tersemat",
"allowInsecure": "Izinkan tidak aman",
"echConfigList": "Daftar konfig ECH",
"muxParams": "Mux",
"sockoptParams": "Sockopt",
"finalMask": "Final Mask",
+30 -2
View File
@@ -475,6 +475,36 @@
"form": {
"echSockopt": "ECH Sockopt",
"echSockoptTip": "Xray が ECH config リストを取得するために使用する接続のソケットオプション(例: ルックアップを dialerProxy アウトバウンド経由にする)。既定値を使う場合は無効のままにします。",
"echCdnWarningTitle": "ローカルで生成した ECH 鍵は直接接続でのみ有効です",
"echCdnWarningBody": "「新しい ECH 証明書を取得」で生成した鍵ペアは、クライアントがこの VPS に直接接続する場合にのみ有効です。インバウンドが Cloudflare CDN 経由(オレンジクラウド背後の VLESS + xhttp/WS)でプロキシされている場合、Cloudflare は外側の TLS を自前の ECH 鍵で終端し、それらをローテーションするため、ローカルの鍵ではハンドシェイクが必ず失敗します。代わりに動的 ECH ヘルパーを使用してください。",
"echDynamicTitle": "Cloudflare 動的 ECH(オレンジクラウドの背後でも利用可能)",
"echPresetLabel": "プリセットの組み合わせ",
"echPresetOtherDomains": "その他の ECH 対象ドメイン",
"echPresetAdvanced": "カスタムの組み合わせ…",
"echDynamicIntro": "ECH config リストにクライアント側の動的参照(対象ドメイン + DoH リゾルバー)を設定します。クライアントは接続のたびにリゾルバーへそのドメインの HTTPS レコードを問い合わせるため、常に最新の ECH 鍵(Cloudflare がローテーションしたものを含む)を取得できます。",
"echDynamicDomain": "ECH 対象ドメイン",
"echDynamicDoh": "DoH リゾルバー",
"echDomainAuto": "自動 — ノードの SNI から ECH レコードを解決",
"echDynamicApply": "ECH config リストに適用",
"echDynamicCustom": "カスタム URL…",
"echDynamicInvalid": "値を生成するには、有効なドメインとリゾルバーの URL(https、h2c または udp)を入力してください。",
"echDynamicUnsupportedScheme": "Xray が ECH 設定を解決できるのは https://、h2c://、udp:// のみです。このエンドポイントは応答しません。",
"echDynamicPlaceholder": "cloudflare-ech.com+https://dns.alidns.com/dns-query",
"echDynamicOverwriteTitle": "このインバウンドにはローカルで生成した ECH 鍵ペアがあります",
"echDynamicOverwriteBody": "CDN の背後ではこの鍵ペアは使用されません。CDN が TLS ハンドシェイクを終端するためです。動的な値を適用すると ECH config リストが置き換えられ、ローカルの ECH 鍵は消去されます。",
"echDynamicOverwriteConfirm": "置き換えてローカル鍵を消去",
"echExistingNotDynamic": "この ECH config リストは動的フォームの形式ではありません。値を入力すると手動で設定した内容は上書きされます。",
"echDohGroup": {
"china": "中国本土(GFW の背後から到達可能)",
"global": "グローバル",
"europe": "ヨーロッパ",
"udp": "通常の DNS(udp://)"
},
"echRecommended": "推奨",
"echMainlandAdvisory": "中国本土のリゾルバーでは、HTTPS レコードに ECH を含む対象ドメインが必要です。保存する前にこの組み合わせを確認してください。",
"echConfigList": "ECH config リスト",
"echConfigListHint": "共有リンクとサブスクリプションでクライアントに送信されます。ローカルの ECHConfigList(下のボタンで生成)または動的フォームの domain+DoH URL を受け付けます。",
"echKeyHint": "直接接続専用です — インバウンドが CDN の背後にある場合は空欄にしてください。",
"curvePreferences": "曲線の優先設定",
"curvePreferencesTip": "サーバーが提供する TLS 鍵交換の曲線を優先順位順に制限します(例: X25519MLKEM768, X25519)。空欄にすると Xray-core の既定値を使用します。",
"masterKeyLog": "マスターキーログ",
@@ -632,7 +662,6 @@
"usageOption": "使用オプション",
"buildChain": "Build Chain",
"echKey": "ECH key",
"echConfig": "ECH config",
"pinnedPeerCertSha256": "ピン留めピア証明書 SHA-256",
"pinnedPeerCertSha256Tip": "ピア証明書の SHA-256 ハッシュ(16進数文字列、例: e8e2d3…)、カンマ区切り。パネルのみ — サーバーの xray 設定には書き込まれませんが、共有リンクには含まれ、クライアントが証明書をピン留めできます。",
"pinnedPeerCertSha256Placeholder": "16進ハッシュ、カンマ区切り",
@@ -2361,7 +2390,6 @@
"pins": "ピン留め証明書 SHA-256",
"verifyPeerCertByName": "名前でピア証明書を検証",
"allowInsecure": "安全でない接続を許可",
"echConfigList": "ECH config リスト",
"muxParams": "Mux",
"sockoptParams": "Sockopt",
"finalMask": "Final Mask",
+30 -2
View File
@@ -475,6 +475,36 @@
"form": {
"echSockopt": "Sockopt ECH",
"echSockoptTip": "Opções de socket para a conexão que o Xray usa ao buscar a lista de configurações ECH (por exemplo, rotear a consulta por um outbound dialerProxy). Deixe desativado para usar os padrões.",
"echCdnWarningTitle": "Chaves ECH geradas localmente só funcionam em conexões diretas",
"echCdnWarningBody": "Um par de chaves criado com «Get New ECH Cert» só é válido quando os clientes se conectam diretamente a este VPS. Se o inbound for proxiado pelo Cloudflare CDN (VLESS + xhttp/WS atrás da nuvem laranja), o Cloudflare encerra o TLS externo com as próprias chaves ECH e as rotaciona, então uma chave local sempre falha no handshake. Use o assistente de ECH dinâmico em vez disso.",
"echDynamicTitle": "ECH dinâmico do Cloudflare (funciona atrás da nuvem laranja)",
"echPresetLabel": "Combinação predefinida",
"echPresetOtherDomains": "Outros domínios de destino ECH",
"echPresetAdvanced": "Combinação personalizada…",
"echDynamicIntro": "Preenche a lista de configurações ECH com uma consulta dinâmica do lado do cliente (domínio de destino + resolvedor DoH). O cliente consulta o resolvedor pelo registro HTTPS do domínio a cada conexão, então sempre vê as chaves ECH atuais — incluindo as rotacionadas do Cloudflare.",
"echDynamicDomain": "Domínio de destino ECH",
"echDynamicDoh": "Resolvedor DoH",
"echDomainAuto": "Automático — resolver o registro ECH do SNI do nó",
"echDynamicApply": "Aplicar à lista de configurações ECH",
"echDynamicCustom": "URL personalizada…",
"echDynamicInvalid": "Informe um domínio válido e uma URL de resolvedor (https, h2c ou udp) para montar o valor.",
"echDynamicUnsupportedScheme": "O Xray só resolve a configuração ECH via https://, h2c:// ou udp://. Este destino nunca responderá.",
"echDynamicPlaceholder": "cloudflare-ech.com+https://dns.alidns.com/dns-query",
"echDynamicOverwriteTitle": "Este inbound tem um par de chaves ECH gerado localmente",
"echDynamicOverwriteBody": "Atrás de um CDN esse par de chaves nunca é usado: o CDN encerra o handshake TLS. Aplicar o valor dinâmico substituirá a lista de configurações ECH e apagará a chave ECH local.",
"echDynamicOverwriteConfirm": "Substituir e apagar chave local",
"echExistingNotDynamic": "Esta lista de configurações ECH não está na forma dinâmica; preenchê-la substituirá o que você definiu manualmente.",
"echDohGroup": {
"china": "China continental (acessível atrás da GFW)",
"global": "Mundial",
"europe": "Europa",
"udp": "DNS simples (udp://)"
},
"echRecommended": "recomendado",
"echMainlandAdvisory": "Resolvedores da China continental exigem um domínio de destino cujo registro HTTPS carregue ECH; verifique o par antes de salvar.",
"echConfigList": "Lista de configurações ECH",
"echConfigListHint": "Enviada aos clientes nos links de compartilhamento e nas assinaturas. Aceita um ECHConfigList local (do botão abaixo) ou a forma dinâmica domínio+URL de DoH.",
"echKeyHint": "Somente para conexões diretas — deixe vazio quando o inbound estiver atrás de um CDN.",
"curvePreferences": "Preferências de curva",
"curvePreferencesTip": "Restringe as curvas de troca de chaves TLS que o servidor oferece, em ordem de preferência (por exemplo, X25519MLKEM768, X25519). Deixe vazio para usar os padrões do Xray-core.",
"masterKeyLog": "Log da chave mestra",
@@ -632,7 +662,6 @@
"usageOption": "Opção de uso",
"buildChain": "Construir cadeia",
"echKey": "ECH key",
"echConfig": "Config ECH",
"pinnedPeerCertSha256": "SHA-256 do cert. do par fixado",
"pinnedPeerCertSha256Tip": "Hashes SHA-256 do certificado do par como string hexadecimal (ex. e8e2d3…), separados por vírgula. Apenas no painel — não é gravado na config xray do servidor, mas é incluído nos links de compartilhamento para que clientes possam fixar o certificado.",
"pinnedPeerCertSha256Placeholder": "hash(es) hexadecimal, separados por vírgula",
@@ -2361,7 +2390,6 @@
"pins": "SHA-256 do certificado fixado",
"verifyPeerCertByName": "Verificar certificado do par pelo nome",
"allowInsecure": "Permitir inseguro",
"echConfigList": "Lista de configurações ECH",
"muxParams": "Mux",
"sockoptParams": "Sockopt",
"finalMask": "Final Mask",
+30 -2
View File
@@ -477,6 +477,36 @@
"autoFill": "Автозаполнение",
"echSockopt": "ECH Sockopt",
"echSockoptTip": "Параметры сокета для соединения, через которое Xray получает список конфигураций ECH (например, можно направить запрос через исходящее dialerProxy). Оставьте отключённым для значений по умолчанию.",
"echCdnWarningTitle": "Локально созданные ключи ECH работают только при прямом подключении",
"echCdnWarningBody": "Пара ключей, созданная кнопкой «Get New ECH Cert», действительна только когда клиенты подключаются напрямую к этому VPS. Если inbound проксируется через Cloudflare CDN (VLESS + xhttp/WS за оранжевым облаком), Cloudflare завершает внешний TLS своими ключами ECH и ротирует их, поэтому локальный ключ всегда проваливает рукопожатие. Вместо него используйте помощник динамического ECH.",
"echDynamicTitle": "Динамический ECH через Cloudflare (работает за оранжевым облаком)",
"echPresetLabel": "Готовая комбинация",
"echPresetOtherDomains": "Другие целевые домены ECH",
"echPresetAdvanced": "Своя комбинация…",
"echDynamicIntro": "Заполняет список конфигураций ECH динамическим запросом на стороне клиента (целевой домен + DoH-резолвер). Клиент при каждом подключении запрашивает у резолвера HTTPS-запись домена, поэтому всегда видит актуальные ключи ECH — включая ротированные ключи Cloudflare.",
"echDynamicDomain": "Целевой домен ECH",
"echDynamicDoh": "DoH-резолвер",
"echDomainAuto": "Автоматически — разрешить запись ECH для SNI узла",
"echDynamicApply": "Применить к списку конфигураций ECH",
"echDynamicCustom": "Свой URL…",
"echDynamicInvalid": "Введите корректный домен и URL резолвера (https, h2c или udp), чтобы собрать значение.",
"echDynamicUnsupportedScheme": "Xray разрешает конфигурацию ECH только через https://, h2c:// или udp://. Этот адрес никогда не ответит.",
"echDynamicPlaceholder": "cloudflare-ech.com+https://dns.alidns.com/dns-query",
"echDynamicOverwriteTitle": "У этого inbound есть локально созданная пара ключей ECH",
"echDynamicOverwriteBody": "За CDN эта пара ключей не используется: TLS-рукопожатие завершает CDN. Применение динамического значения заменит список конфигураций ECH и удалит локальный ключ ECH.",
"echDynamicOverwriteConfirm": "Заменить и удалить локальный ключ",
"echExistingNotDynamic": "Этот список конфигураций ECH не в динамической форме; при заполнении он заменит то, что вы задали вручную.",
"echDohGroup": {
"china": "Материковый Китай (доступен из-за GFW)",
"global": "Весь мир",
"europe": "Европа",
"udp": "Обычный DNS (udp://)"
},
"echRecommended": "рекомендуется",
"echMainlandAdvisory": "Резолверам материкового Китая нужен целевой домен, в HTTPS-записи которого есть ECH; проверьте пару перед сохранением.",
"echConfigList": "Список конфигураций ECH",
"echConfigListHint": "Передаётся клиентам в ссылках для обмена и подписках. Принимает локальный ECHConfigList (из кнопки ниже) или динамическую форму домен+URL DoH.",
"echKeyHint": "Только для прямых подключений — оставьте пустым, если inbound находится за CDN.",
"curvePreferences": "Предпочтения кривых",
"curvePreferencesTip": "Ограничьте кривые обмена ключами TLS, которые предлагает сервер, в порядке предпочтения (например, X25519MLKEM768, X25519). Оставьте пустым для значений по умолчанию Xray-core.",
"masterKeyLog": "Лог мастер-ключей",
@@ -634,7 +664,6 @@
"usageOption": "Опция использования",
"buildChain": "Build Chain",
"echKey": "ECH key",
"echConfig": "ECH config",
"pinnedPeerCertSha256": "SHA-256 сертификата пира",
"pinnedPeerCertSha256Tip": "SHA-256-хеши сертификата пира в виде шестнадцатеричной строки (напр. e8e2d3…), через запятую. Только для панели — не записывается в конфиг xray сервера, но включается в ссылки-приглашения, чтобы клиенты могли закрепить сертификат.",
"pinnedPeerCertSha256Placeholder": "шестнадцатеричный хеш(и), через запятую",
@@ -2361,7 +2390,6 @@
"pins": "Закреплённый SHA-256 сертификата",
"verifyPeerCertByName": "Проверять сертификат пира по имени",
"allowInsecure": "Разрешить небезопасное",
"echConfigList": "Список конфигураций ECH",
"muxParams": "Mux",
"sockoptParams": "Sockopt",
"finalMask": "Final Mask",
+30 -2
View File
@@ -611,7 +611,6 @@
"usageOption": "Kullanım Seçeneği",
"buildChain": "Zincir Oluştur",
"echKey": "ECH Key",
"echConfig": "ECH Yapılandırması",
"pinnedPeerCertSha256": "Sabitlenmiş Peer Sertifikası SHA-256",
"pinnedPeerCertSha256Tip": "Peer sertifikasının SHA-256 hash'leri onaltılık (hex) dizge olarak (örn. e8e2d3…), virgülle ayrılmış. Sadece panel — sunucunun Xray yapılandırmasına yazılmaz, ancak kullanıcıların sertifikayı sabitleyebilmesi için paylaşım bağlantılarına eklenir.",
"pinnedPeerCertSha256Placeholder": "onaltılık (hex) hash(ler), virgülle ayrılmış",
@@ -679,6 +678,36 @@
},
"echSockopt": "ECH Sockopt",
"echSockoptTip": "Xray'in ECH yapılandırma listesini almak için kullandığı bağlantının soket seçenekleri (ör. aramayı bir dialerProxy giden bağlantısı üzerinden yönlendirme). Varsayılanları kullanmak için devre dışı bırakın.",
"echCdnWarningTitle": "Yerel olarak oluşturulan ECH anahtarları yalnızca doğrudan bağlantılarda çalışır",
"echCdnWarningBody": "«Get New ECH Cert» ile oluşturulan anahtar çifti yalnızca istemciler doğrudan bu VPS'e bağlandığında geçerlidir. Inbound, Cloudflare CDN üzerinden proxyleniyorsa (turuncu bulutun arkasında VLESS + xhttp/WS), Cloudflare dış TLS'i kendi ECH anahtarlarıyla sonlandırır ve bunları döndürür; bu yüzden yerel anahtar el sıkışmasında hep başarısız olur. Bunun yerine Dinamik ECH yardımcısını kullanın.",
"echDynamicTitle": "Cloudflare dinamik ECH (turuncu bulutun arkasında çalışır)",
"echPresetLabel": "Hazır kombinasyon",
"echPresetOtherDomains": "Diğer ECH hedef alan adları",
"echPresetAdvanced": "Özel kombinasyon…",
"echDynamicIntro": "ECH yapılandırma listesini istemci tarafında dinamik bir sorguyla doldurur (hedef alan adı + DoH çözümleyici). İstemci her bağlantıda çözümleyiciye alan adının HTTPS kaydını sorar, böylece her zaman güncel ECH anahtarlarını görür — Cloudflare'in döndürdüğü anahtarlar dahil.",
"echDynamicDomain": "ECH hedef alan adı",
"echDynamicDoh": "DoH çözümleyici",
"echDomainAuto": "Otomatik — düğümün SNI değeri için ECH kaydını çöz",
"echDynamicApply": "ECH yapılandırma listesine uygula",
"echDynamicCustom": "Özel URL…",
"echDynamicInvalid": "Değeri oluşturmak için geçerli bir alan adı ve bir çözümleyici URL'si (https, h2c veya udp) girin.",
"echDynamicUnsupportedScheme": "Xray ECH yapılandırmasını yalnızca https://, h2c:// veya udp:// üzerinden çözer. Bu uç nokta hiçbir zaman yanıt vermez.",
"echDynamicPlaceholder": "cloudflare-ech.com+https://dns.alidns.com/dns-query",
"echDynamicOverwriteTitle": "Bu inbound'da yerel olarak oluşturulmuş bir ECH anahtar çifti var",
"echDynamicOverwriteBody": "Bir CDN'in arkasında bu anahtar çifti hiç kullanılmaz: TLS el sıkışmasını CDN sonlandırır. Dinamik değeri uygulamak ECH yapılandırma listesini değiştirir ve yerel ECH anahtarını siler.",
"echDynamicOverwriteConfirm": "Değiştir ve yerel anahtarı sil",
"echExistingNotDynamic": "Bu ECH yapılandırma listesi dinamik biçimde değil; doldurmak elle ayarladığınız değerlerin yerine geçer.",
"echDohGroup": {
"china": "Çin anakarası (GFW'in arkasından erişilebilir)",
"global": "Küresel",
"europe": "Avrupa",
"udp": "Düz DNS (udp://)"
},
"echRecommended": "önerilir",
"echMainlandAdvisory": "Çin anakarasındaki çözümleyiciler, HTTPS kaydı ECH taşıyan bir hedef alan adı gerektirir; kaydetmeden önce kombinasyonu doğrulayın.",
"echConfigList": "ECH yapılandırma listesi",
"echConfigListHint": "Paylaşım bağlantılarında ve aboneliklerde istemcilere gönderilir. Yerel bir ECHConfigList (aşağıdaki düğmeden) veya dinamik alan adı+DoH URL biçimini kabul eder.",
"echKeyHint": "Yalnızca doğrudan bağlantılar için — inbound bir CDN'in arkasındayken boş bırakın.",
"curvePreferences": "Eğri Tercihleri",
"curvePreferencesTip": "Sunucunun sunduğu TLS anahtar değişim eğrilerini tercih sırasına göre kısıtlayın (ör. X25519MLKEM768, X25519). Xray-core varsayılanlarını kullanmak için boş bırakın.",
"masterKeyLog": "Ana Anahtar Günlüğü",
@@ -2360,7 +2389,6 @@
"fingerprint": "Parmak İzi",
"pins": "Sabitlenmiş sertifika SHA-256",
"allowInsecure": "Güvensize izin ver",
"echConfigList": "ECH yapılandırma listesi",
"muxParams": "Ağız",
"sockoptParams": "Sockopt",
"finalMask": "Son Maske",
+30 -2
View File
@@ -611,7 +611,6 @@
"usageOption": "Опція використання",
"buildChain": "Build Chain",
"echKey": "ECH key",
"echConfig": "ECH config",
"pinnedPeerCertSha256": "Закріплений SHA-256 сертифіката пира",
"pinnedPeerCertSha256Tip": "SHA-256-хеші сертифіката пира у вигляді шістнадцяткового рядка (напр. e8e2d3…), через кому. Лише панель — не записується в конфіг xray сервера, але додається до посилань спільного доступу, щоб клієнти могли закріпити сертифікат.",
"pinnedPeerCertSha256Placeholder": "шістнадцятковий хеш(і), через кому",
@@ -674,6 +673,36 @@
"disableFlowHelp": "Виключити цей inbound з автоматичного додавання xtls-rprx-vision, навіть якщо його транспорт підтримує flow (наприклад, тунельований XHTTP inbound із шифруванням VLESS). Клієнти зберігають Vision на інших сумісних inbound у тій самій підписці. Лише VLESS.",
"echSockopt": "ECH Sockopt",
"echSockoptTip": "Параметри сокета для з'єднання, яке Xray використовує для отримання списку конфігурацій ECH (наприклад, спрямувати запит через вихідний dialerProxy). Залиште вимкненим, щоб використовувати типові значення.",
"echCdnWarningTitle": "Локально створені ключі ECH працюють лише за прямого з'єднання",
"echCdnWarningBody": "Пара ключів, створена кнопкою «Get New ECH Cert», дійсна лише коли клієнти підключаються безпосередньо до цього VPS. Якщо inbound проксіюється через Cloudflare CDN (VLESS + xhttp/WS за помаранчевою хмарою), Cloudflare завершує зовнішній TLS власними ключами ECH і ротує їх, тому локальний ключ завжди провалює рукостискання. Натомість скористайтеся помічником динамічного ECH.",
"echDynamicTitle": "Динамічний ECH через Cloudflare (працює за помаранчевою хмарою)",
"echPresetLabel": "Готова комбінація",
"echPresetOtherDomains": "Інші цільові домени ECH",
"echPresetAdvanced": "Власна комбінація…",
"echDynamicIntro": "Заповнює список конфігурацій ECH динамічним запитом на боці клієнта (цільовий домен + DoH-резолвер). Клієнт при кожному з'єднанні запитує в резолвера HTTPS-запис домену, тож завжди бачить актуальні ключі ECH — зокрема ротовані ключі Cloudflare.",
"echDynamicDomain": "Цільовий домен ECH",
"echDynamicDoh": "DoH-резолвер",
"echDomainAuto": "Автоматично — розв'язати запис ECH для SNI вузла",
"echDynamicApply": "Застосувати до списку конфігурацій ECH",
"echDynamicCustom": "Власний URL…",
"echDynamicInvalid": "Введіть коректний домен і URL резолвера (https, h2c або udp), щоб сформувати значення.",
"echDynamicUnsupportedScheme": "Xray розв'язує конфігурацію ECH лише через https://, h2c:// або udp://. Ця адреса ніколи не відповість.",
"echDynamicPlaceholder": "cloudflare-ech.com+https://dns.alidns.com/dns-query",
"echDynamicOverwriteTitle": "Цей inbound має локально створену пару ключів ECH",
"echDynamicOverwriteBody": "За CDN ця пара ключів не використовується: рукостискання TLS завершує CDN. Застосування динамічного значення замінить список конфігурацій ECH і вилучить локальний ключ ECH.",
"echDynamicOverwriteConfirm": "Замінити й вилучити локальний ключ",
"echExistingNotDynamic": "Цей список конфігурацій ECH не в динамічній формі; заповнення замінить те, що ви задали вручну.",
"echDohGroup": {
"china": "Материковий Китай (доступний з-за GFW)",
"global": "Увесь світ",
"europe": "Європа",
"udp": "Звичайний DNS (udp://)"
},
"echRecommended": "рекомендовано",
"echMainlandAdvisory": "Резолверам материкового Китаю потрібен цільовий домен, чий HTTPS-запис містить ECH; перевірте пару перед збереженням.",
"echConfigList": "Список конфігурацій ECH",
"echConfigListHint": "Передається клієнтам у посиланнях для обміну та підписках. Приймає локальний ECHConfigList (з кнопки нижче) або динамічну форму домен+URL DoH.",
"echKeyHint": "Лише для прямих з'єднань — залиште порожнім, якщо inbound стоїть за CDN.",
"curvePreferences": "Налаштування кривих",
"curvePreferencesTip": "Обмежує криві обміну ключами TLS, які пропонує сервер, у порядку переваги (наприклад, X25519MLKEM768, X25519). Залиште порожнім, щоб використовувати типові значення Xray-core.",
"masterKeyLog": "Журнал майстер-ключів",
@@ -2361,7 +2390,6 @@
"pins": "Закріплений SHA-256 сертифіката",
"verifyPeerCertByName": "Перевіряти сертифікат пира за іменем",
"allowInsecure": "Дозволити небезпечне",
"echConfigList": "Список конфігурацій ECH",
"muxParams": "Mux",
"sockoptParams": "Sockopt",
"finalMask": "Фінальна маска",
+30 -2
View File
@@ -475,6 +475,36 @@
"form": {
"echSockopt": "ECH Sockopt",
"echSockoptTip": "Tùy chọn socket cho kết nối mà Xray dùng để lấy danh sách cấu hình ECH (ví dụ định tuyến truy vấn qua một outbound dialerProxy). Để tắt để dùng giá trị mặc định.",
"echCdnWarningTitle": "Khóa ECH tạo cục bộ chỉ dùng được cho kết nối trực tiếp",
"echCdnWarningBody": "Cặp khóa tạo bằng «Get New ECH Cert» chỉ hợp lệ khi máy khách kết nối thẳng tới VPS này. Nếu inbound được proxy qua Cloudflare CDN (VLESS + xhttp/WS sau đám mây màu cam), Cloudflare kết thúc TLS lớp ngoài bằng khóa ECH của chính nó và luân chuyển chúng, nên khóa cục bộ luôn thất bại khi bắt tay. Hãy dùng trợ lý ECH động thay thế.",
"echDynamicTitle": "ECH động Cloudflare (hoạt động sau đám mây màu cam)",
"echPresetLabel": "Tổ hợp có sẵn",
"echPresetOtherDomains": "Miền đích ECH khác",
"echPresetAdvanced": "Tổ hợp tùy chỉnh…",
"echDynamicIntro": "Điền danh sách cấu hình ECH bằng một truy vấn động ở phía máy khách (miền đích + trình phân giải DoH). Mỗi lần kết nối, máy khách hỏi trình phân giải bản ghi HTTPS của miền, nên luôn thấy khóa ECH hiện hành — kể cả khóa đã luân chuyển của Cloudflare.",
"echDynamicDomain": "Miền đích ECH",
"echDynamicDoh": "Trình phân giải DoH",
"echDomainAuto": "Tự động — phân giải bản ghi ECH cho SNI của nút",
"echDynamicApply": "Áp dụng vào danh sách cấu hình ECH",
"echDynamicCustom": "URL tùy chỉnh…",
"echDynamicInvalid": "Hãy nhập một miền hợp lệ và URL trình phân giải (https, h2c hoặc udp) để tạo giá trị.",
"echDynamicUnsupportedScheme": "Xray chỉ phân giải cấu hình ECH qua https://, h2c:// hoặc udp://. Điểm cuối này sẽ không bao giờ phản hồi.",
"echDynamicPlaceholder": "cloudflare-ech.com+https://dns.alidns.com/dns-query",
"echDynamicOverwriteTitle": "Inbound này có cặp khóa ECH tạo cục bộ",
"echDynamicOverwriteBody": "Sau một CDN, cặp khóa đó không bao giờ được dùng: CDN là bên kết thúc bắt tay TLS. Áp dụng giá trị động sẽ thay thế danh sách cấu hình ECH và xóa khóa ECH cục bộ.",
"echDynamicOverwriteConfirm": "Thay thế và xóa khóa cục bộ",
"echExistingNotDynamic": "Danh sách cấu hình ECH này không ở dạng động; điền vào sẽ thay thế những gì bạn đã đặt thủ công.",
"echDohGroup": {
"china": "Trung Quốc đại lục (truy cập được từ sau GFW)",
"global": "Toàn cầu",
"europe": "Châu Âu",
"udp": "DNS thường (udp://)"
},
"echRecommended": "khuyến nghị",
"echMainlandAdvisory": "Trình phân giải trong Trung Quốc đại lục cần một miền đích có bản ghi HTTPS mang ECH; hãy tự kiểm tra cặp này trước khi lưu.",
"echConfigList": "Danh sách cấu hình ECH",
"echConfigListHint": "Được gửi tới máy khách trong liên kết chia sẻ và gói đăng ký. Chấp nhận ECHConfigList cục bộ (từ nút bên dưới) hoặc dạng động miền+URL DoH.",
"echKeyHint": "Chỉ dành cho kết nối trực tiếp — để trống khi inbound nằm sau CDN.",
"curvePreferences": "Ưu tiên đường cong",
"curvePreferencesTip": "Giới hạn các đường cong trao đổi khóa TLS mà máy chủ cung cấp, theo thứ tự ưu tiên (ví dụ X25519MLKEM768, X25519). Để trống để dùng giá trị mặc định của Xray-core.",
"masterKeyLog": "Nhật ký khóa chính",
@@ -632,7 +662,6 @@
"usageOption": "Tùy chọn sử dụng",
"buildChain": "Tạo chuỗi",
"echKey": "ECH key",
"echConfig": "Cấu hình ECH",
"pinnedPeerCertSha256": "SHA-256 chứng chỉ peer đã ghim",
"pinnedPeerCertSha256Tip": "Hash SHA-256 của chứng chỉ peer dưới dạng chuỗi thập lục phân (vd. e8e2d3…), phân tách bằng dấu phẩy. Chỉ panel — không ghi vào cấu hình xray máy chủ, nhưng được đưa vào liên kết chia sẻ để client có thể ghim chứng chỉ.",
"pinnedPeerCertSha256Placeholder": "hash thập lục phân, phân tách bằng dấu phẩy",
@@ -2361,7 +2390,6 @@
"pins": "SHA-256 chứng chỉ đã ghim",
"verifyPeerCertByName": "Xác minh chứng chỉ peer theo tên",
"allowInsecure": "Cho phép không an toàn",
"echConfigList": "Danh sách cấu hình ECH",
"muxParams": "Mux",
"sockoptParams": "Sockopt",
"finalMask": "Final Mask",
+30 -2
View File
@@ -475,6 +475,36 @@
"form": {
"echSockopt": "ECH Sockopt",
"echSockoptTip": "Xray 获取 ECH 配置列表时所用连接的 Socket 选项(例如让该查询通过 dialerProxy 出站)。保持禁用则使用默认值。",
"echCdnWarningTitle": "本地生成的 ECH 密钥仅适用于直连",
"echCdnWarningBody": "「获取新 ECH 证书」生成的密钥对,只有在客户端直连本机 VPS 时才有效。若节点经 Cloudflare CDN 代理(小黄云后的 VLESS + xhttp/WS),外层 TLS 由 Cloudflare 用自己的 ECH 密钥终结且会定期轮换,本地密钥必然导致握手失败。此场景请改用动态 ECH 辅助器。",
"echDynamicTitle": "Cloudflare 动态 ECH(小黄云可用)",
"echPresetLabel": "预设组合",
"echPresetOtherDomains": "其它 ECH 解析域名",
"echPresetAdvanced": "自定义组合…",
"echDynamicIntro": "把「解析域名 + DoH 服务」自动拼装成客户端动态查询格式并填入 ECH 配置列表。客户端每次连接都会向所选的 DoH 服务查询该域名的 HTTPS 记录,因此始终拿到最新的 ECH 密钥——包括 Cloudflare 轮换后的密钥。",
"echDynamicDomain": "ECH 解析域名",
"echDynamicDoh": "DoH 服务",
"echDomainAuto": "自动获取(使用节点的伪装域名解析 EchConfig)",
"echDynamicApply": "应用到 ECH 配置列表",
"echDynamicCustom": "自定义 URL…",
"echDynamicInvalid": "请填写有效的域名与 DoH 地址(仅支持 https、h2c、udp)后再生成。",
"echDynamicUnsupportedScheme": "Xray 仅支持通过 https://、h2c:// 或 udp:// 解析 ECH 配置,该地址永远不会返回结果。",
"echDynamicPlaceholder": "cloudflare-ech.com+https://dns.alidns.com/dns-query",
"echDynamicOverwriteTitle": "该入站已存在本地生成的 ECH 密钥对",
"echDynamicOverwriteBody": "在 CDN 之后,这对密钥根本不会被使用:TLS 握手由 CDN 终止。应用动态值将覆盖 ECH 配置列表,并清空本地 ECH 私钥。",
"echDynamicOverwriteConfirm": "覆盖并清空本地密钥",
"echExistingNotDynamic": "当前 ECH 配置不是动态格式,填入会覆盖你手工设置的值。",
"echDohGroup": {
"china": "中国大陆(墙内可直连)",
"global": "全球",
"europe": "欧洲",
"udp": "普通 DNS(udp://)"
},
"echRecommended": "推荐",
"echMainlandAdvisory": "国内 DNS 需搭配能返回 ECH 的解析域名;是否可用请保存前自行验证。",
"echConfigList": "ECH 配置列表",
"echConfigListHint": "会随分享链接与订阅下发给客户端。可填本地 ECHConfigList(即下方按钮的产物),也可填动态格式 domain+DoH URL。",
"echKeyHint": "仅限直连使用;节点位于 CDN 之后时请留空。",
"curvePreferences": "曲线偏好",
"curvePreferencesTip": "限制服务器提供的 TLS 密钥交换曲线,并按偏好顺序排列(例如 X25519MLKEM768、X25519)。留空则使用 Xray-core 默认值。",
"masterKeyLog": "主密钥日志",
@@ -631,7 +661,6 @@
"usageOption": "使用选项",
"buildChain": "构建证书链",
"echKey": "ECH key",
"echConfig": "ECH 配置",
"pinnedPeerCertSha256": "固定对端证书 SHA-256",
"pinnedPeerCertSha256Tip": "对端证书的 SHA-256 哈希(十六进制字符串,如 e8e2d3…),逗号分隔。仅面板使用 — 不写入服务器的 xray 配置,但会包含在分享链接中,以便客户端固定证书。",
"pinnedPeerCertSha256Placeholder": "十六进制哈希,逗号分隔",
@@ -2361,7 +2390,6 @@
"pins": "固定证书 SHA-256",
"verifyPeerCertByName": "按名称验证对端证书",
"allowInsecure": "允许不安全连接",
"echConfigList": "ECH 配置列表",
"muxParams": "Mux",
"sockoptParams": "Sockopt",
"finalMask": "Final Mask",
+30 -2
View File
@@ -611,7 +611,6 @@
"usageOption": "使用選項",
"buildChain": "建立憑證鏈",
"echKey": "ECH key",
"echConfig": "ECH 設定",
"pinnedPeerCertSha256": "釘選對端憑證 SHA-256",
"pinnedPeerCertSha256Tip": "對端憑證的 SHA-256 雜湊(十六進位字串,如 e8e2d3…),以逗號分隔。僅面板使用 — 不寫入伺服器的 xray 設定,但會包含在分享連結中,以便用戶端釘選憑證。",
"pinnedPeerCertSha256Placeholder": "十六進位雜湊,以逗號分隔",
@@ -663,6 +662,36 @@
"getNewSeed": "取得新 Seed",
"echSockopt": "ECH Sockopt",
"echSockoptTip": "Xray 用來取得 ECH 設定清單的連線之通訊端選項(例如讓查詢透過 dialerProxy 出站)。停用則使用預設值。",
"echCdnWarningTitle": "在本機產生的 ECH 金鑰僅適用於直接連線",
"echCdnWarningBody": "由「取得新 ECH 憑證」產生的金鑰組,只有在用戶端直接連線到這台 VPS 時才有效。若入站是透過 Cloudflare CDN 代理(橘色雲朵後的 VLESS + xhttp/WS),Cloudflare 會以自己的 ECH 金鑰終止外層 TLS 並定期輪換,因此本機金鑰必定無法完成握手。請改用動態 ECH 輔助器。",
"echDynamicTitle": "Cloudflare 動態 ECH(橘色雲朵後方亦可使用)",
"echPresetLabel": "預設組合",
"echPresetOtherDomains": "其他 ECH 目標網域",
"echPresetAdvanced": "自訂組合…",
"echDynamicIntro": "以用戶端動態查詢(目標網域 + DoH 解析器)填入 ECH 設定清單。用戶端每次連線時都會向解析器查詢該網域的 HTTPS 記錄,因此永遠能取得目前的 ECH 金鑰,包括 Cloudflare 輪換過的金鑰。",
"echDynamicDomain": "ECH 目標網域",
"echDynamicDoh": "DoH 解析器",
"echDomainAuto": "自動 — 依節點的 SNI 解析 ECH 記錄",
"echDynamicApply": "套用至 ECH 設定清單",
"echDynamicCustom": "自訂 URL…",
"echDynamicInvalid": "請輸入有效的網域與解析器 URL(僅支援 https、h2c、udp)以產生此值。",
"echDynamicUnsupportedScheme": "Xray 僅支援透過 https://、h2c:// 或 udp:// 解析 ECH 設定,此位址永遠不會回應。",
"echDynamicPlaceholder": "cloudflare-ech.com+https://dns.alidns.com/dns-query",
"echDynamicOverwriteTitle": "此入站已有在本機產生的 ECH 金鑰組",
"echDynamicOverwriteBody": "在 CDN 後方這組金鑰永遠不會被使用:CDN 會終止 TLS 握手。套用動態值將取代 ECH 設定清單並清除本機 ECH 金鑰。",
"echDynamicOverwriteConfirm": "取代並清除本機金鑰",
"echExistingNotDynamic": "此 ECH 設定清單並非動態格式;填入值將會覆寫你手動設定的內容。",
"echDohGroup": {
"china": "中國大陸(可從 GFW 後方連線)",
"global": "全球",
"europe": "歐洲",
"udp": "一般 DNS(udp://)"
},
"echRecommended": "推薦",
"echMainlandAdvisory": "中國大陸的解析器需要一個 HTTPS 記錄帶有 ECH 的目標網域;儲存前請先驗證此組合。",
"echConfigList": "ECH 設定清單",
"echConfigListHint": "隨分享連結與訂閱傳送給用戶端。可接受本機的 ECHConfigList(來自下方按鈕),或動態表單的 domain+DoH URL。",
"echKeyHint": "僅供直接連線使用 — 若入站位於 CDN 後方,請留空。",
"curvePreferences": "曲線偏好",
"curvePreferencesTip": "依偏好順序限制伺服器提供的 TLS 金鑰交換曲線(例如 X25519MLKEM768、X25519)。留空則使用 Xray-core 預設值。",
"masterKeyLog": "主金鑰日誌",
@@ -2361,7 +2390,6 @@
"pins": "釘選憑證 SHA-256",
"verifyPeerCertByName": "依名稱驗證對端憑證",
"allowInsecure": "允許不安全連線",
"echConfigList": "ECH 設定清單",
"muxParams": "Mux",
"sockoptParams": "Sockopt",
"finalMask": "Final Mask",