mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 00:24:19 +00:00
feat(mtproto): add domain-fronting and essential mtg options
Expose mtg's [domain-fronting] section (ip/port/proxy-protocol) plus proxy-protocol-listener, prefer-ip, and debug on MTProto inbounds. Each key is written to the generated mtg-<id>.toml only when set, so mtg's own defaults apply otherwise. The instance fingerprint now covers these fields, so editing an option restarts the sidecar. Since MTProto is mtg-served (not Xray), sniffing does not apply: hide the Sniffing tab and the Advanced sniffing sub-editor, drop it from the Advanced "All" JSON view, and emit empty sniffing in the wire payload, all gated by a new canEnableSniffing predicate.
This commit is contained in:
@@ -11,6 +11,7 @@ import type { StreamSettings } from '@/schemas/api/inbound';
|
||||
import type { Sniffing } from '@/schemas/primitives';
|
||||
import type { z } from 'zod';
|
||||
import { normalizeStreamSettingsForWire } from '@/lib/xray/stream-wire-normalize';
|
||||
import { canEnableSniffing } from '@/lib/xray/protocol-capabilities';
|
||||
|
||||
// Plain-data adapter between the panel's stored inbound row shape and
|
||||
// the typed InboundFormValues that Form.useForm<T> carries inside
|
||||
@@ -302,7 +303,9 @@ export function formValuesToWirePayload(values: InboundFormValues): WireInboundP
|
||||
protocol: values.protocol,
|
||||
settings: JSON.stringify(settingsPruned),
|
||||
streamSettings: streamPruned ? JSON.stringify(streamPruned) : '',
|
||||
sniffing: JSON.stringify(normalizeSniffing(values.sniffing)),
|
||||
// mtproto is mtg-served, not Xray, so sniffing never applies — emit empty
|
||||
// rather than the default { enabled: false } so the row carries no sniffing.
|
||||
sniffing: canEnableSniffing({ protocol: values.protocol }) ? JSON.stringify(normalizeSniffing(values.sniffing)) : '',
|
||||
tag: values.tag,
|
||||
};
|
||||
if (values.nodeId != null) payload.nodeId = values.nodeId;
|
||||
|
||||
@@ -50,6 +50,12 @@ export function canEnableStream(values: { protocol: string }): boolean {
|
||||
return STREAM_PROTOCOLS.includes(values.protocol);
|
||||
}
|
||||
|
||||
// mtproto is served by an external mtg process, not Xray, so the Xray sniffing
|
||||
// block does not apply to it. Every other inbound supports sniffing.
|
||||
export function canEnableSniffing(values: { protocol: string }): boolean {
|
||||
return values.protocol !== 'mtproto';
|
||||
}
|
||||
|
||||
// Vision seed applies only when XTLS Vision (TCP/TLS) flow is selected
|
||||
// AND at least one VLESS client uses the vision flow. Excludes UDP variant.
|
||||
export function canEnableVisionSeed(values: CapabilityVlessSlice): boolean {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults';
|
||||
import { composeInboundTag, isAutoInboundTag, type InboundTagInput } from '@/lib/xray/inbound-tag';
|
||||
import {
|
||||
canEnableReality,
|
||||
canEnableSniffing,
|
||||
canEnableStream,
|
||||
canEnableTls,
|
||||
isSS2022,
|
||||
@@ -160,6 +161,7 @@ export default function InboundFormModal({
|
||||
const network = Form.useWatch(['streamSettings', 'network'], form) ?? '';
|
||||
const security = Form.useWatch(['streamSettings', 'security'], form) ?? 'none';
|
||||
const streamEnabled = canEnableStream({ protocol });
|
||||
const sniffingSupported = canEnableSniffing({ protocol });
|
||||
|
||||
const wPort = Form.useWatch('port', form);
|
||||
const wListen = (Form.useWatch('listen', form) ?? '') as string;
|
||||
@@ -776,7 +778,7 @@ export default function InboundFormModal({
|
||||
<div className="advanced-editor-meta">
|
||||
{t('pages.inbounds.advanced.allHelp')}
|
||||
</div>
|
||||
<AdvancedAllEditor form={form} streamEnabled={streamEnabled} />
|
||||
<AdvancedAllEditor form={form} streamEnabled={streamEnabled} sniffingEnabled={sniffingSupported} />
|
||||
</>
|
||||
),
|
||||
},
|
||||
@@ -820,25 +822,27 @@ export default function InboundFormModal({
|
||||
),
|
||||
}]
|
||||
: []),
|
||||
{
|
||||
key: 'sniffing',
|
||||
label: t('pages.inbounds.advanced.sniffing'),
|
||||
children: (
|
||||
<>
|
||||
<div className="advanced-editor-meta">
|
||||
{t('pages.inbounds.advanced.sniffingHelp')}{' '}
|
||||
<code>{'{ sniffing: { ... } }'}</code>.
|
||||
</div>
|
||||
<AdvancedSliceEditor
|
||||
form={form}
|
||||
path="sniffing"
|
||||
wrapKey="sniffing"
|
||||
minHeight="240px"
|
||||
maxHeight="420px"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
...(sniffingSupported
|
||||
? [{
|
||||
key: 'sniffing',
|
||||
label: t('pages.inbounds.advanced.sniffing'),
|
||||
children: (
|
||||
<>
|
||||
<div className="advanced-editor-meta">
|
||||
{t('pages.inbounds.advanced.sniffingHelp')}{' '}
|
||||
<code>{'{ sniffing: { ... } }'}</code>.
|
||||
</div>
|
||||
<AdvancedSliceEditor
|
||||
form={form}
|
||||
path="sniffing"
|
||||
wrapKey="sniffing"
|
||||
minHeight="240px"
|
||||
maxHeight="420px"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -896,7 +900,9 @@ export default function InboundFormModal({
|
||||
{ key: 'security', label: t('pages.inbounds.securityTab'), children: securityTab, forceRender: true },
|
||||
]
|
||||
: []),
|
||||
{ key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: sniffingTab, forceRender: true },
|
||||
...(sniffingSupported
|
||||
? [{ key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: sniffingTab, forceRender: true }]
|
||||
: []),
|
||||
{ key: 'advanced', label: t('pages.xray.advancedTemplate'), children: advancedTab, forceRender: true },
|
||||
]} />
|
||||
</Form>
|
||||
|
||||
@@ -92,9 +92,11 @@ export function AdvancedSliceEditor({
|
||||
export function AdvancedAllEditor({
|
||||
form,
|
||||
streamEnabled,
|
||||
sniffingEnabled,
|
||||
}: {
|
||||
form: FormInstance<InboundFormValues>;
|
||||
streamEnabled: boolean;
|
||||
sniffingEnabled: boolean;
|
||||
}) {
|
||||
// preserve: true — default useWatch returns only registered fields, so
|
||||
// sub-trees we never bound (settings.clients/fallbacks, sniffing
|
||||
@@ -127,8 +129,10 @@ export function AdvancedAllEditor({
|
||||
protocol: wProtocol ?? '',
|
||||
tag: wTag ?? '',
|
||||
settings: settingsView,
|
||||
sniffing: normalizeSniffing(wSniffing as Parameters<typeof normalizeSniffing>[0]),
|
||||
};
|
||||
if (sniffingEnabled) {
|
||||
out.sniffing = normalizeSniffing(wSniffing as Parameters<typeof normalizeSniffing>[0]);
|
||||
}
|
||||
if (streamView) out.streamSettings = streamView;
|
||||
return JSON.stringify(out, null, 2);
|
||||
};
|
||||
@@ -146,7 +150,7 @@ export function AdvancedAllEditor({
|
||||
setText(formStr);
|
||||
lastEmitRef.current = formStr;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [wListen, wPort, wProtocol, wTag, wSettings, wSniffing, wStream, streamEnabled]);
|
||||
}, [wListen, wPort, wProtocol, wTag, wSettings, wSniffing, wStream, streamEnabled, sniffingEnabled]);
|
||||
|
||||
return (
|
||||
<JsonEditor
|
||||
@@ -171,7 +175,7 @@ export function AdvancedAllEditor({
|
||||
if (parsed.settings && typeof parsed.settings === 'object') {
|
||||
form.setFieldValue('settings', parsed.settings);
|
||||
}
|
||||
if (parsed.sniffing && typeof parsed.sniffing === 'object') {
|
||||
if (sniffingEnabled && parsed.sniffing && typeof parsed.sniffing === 'object') {
|
||||
form.setFieldValue('sniffing', parsed.sniffing);
|
||||
}
|
||||
if (streamEnabled && parsed.streamSettings && typeof parsed.streamSettings === 'object') {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Button, Form, Input, Space } from 'antd';
|
||||
import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
|
||||
import { generateMtprotoSecret, mtprotoSecretForDomain } from '@/lib/xray/inbound-defaults';
|
||||
@@ -32,8 +32,44 @@ export default function MtprotoFields() {
|
||||
/>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{ span: 24 }}>
|
||||
<Alert type="info" showIcon message={t('pages.inbounds.form.mtprotoHint')} />
|
||||
<Form.Item
|
||||
name={['settings', 'domainFronting', 'ip']}
|
||||
label={t('pages.inbounds.form.mtgDomainFrontingIp')}
|
||||
tooltip={t('pages.inbounds.form.mtgDomainFrontingHint')}
|
||||
>
|
||||
<Input placeholder="127.0.0.1" />
|
||||
</Form.Item>
|
||||
<Form.Item name={['settings', 'domainFronting', 'port']} label={t('pages.inbounds.form.mtgDomainFrontingPort')}>
|
||||
<InputNumber min={0} max={65535} placeholder="443" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['settings', 'domainFronting', 'proxyProtocol']}
|
||||
label={t('pages.inbounds.form.mtgDomainFrontingProxyProtocol')}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['settings', 'proxyProtocolListener']}
|
||||
label={t('pages.inbounds.form.mtgProxyProtocolListener')}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name={['settings', 'preferIp']} label={t('pages.inbounds.form.mtgPreferIp')}>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="prefer-ipv6"
|
||||
options={[
|
||||
{ value: 'prefer-ipv6', label: 'prefer-ipv6' },
|
||||
{ value: 'prefer-ipv4', label: 'prefer-ipv4' },
|
||||
{ value: 'only-ipv6', label: 'only-ipv6' },
|
||||
{ value: 'only-ipv4', label: 'only-ipv4' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name={['settings', 'debug']} label={t('pages.inbounds.form.mtgDebug')} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -640,6 +640,47 @@ export default function InboundInfoModal({
|
||||
</Tooltip>
|
||||
</dd>
|
||||
</div>
|
||||
{(() => {
|
||||
const s = inbound.settings;
|
||||
const df = s.domainFronting as { ip?: string; port?: number; proxyProtocol?: boolean } | undefined;
|
||||
const frontingTarget = df && (df.ip || df.port)
|
||||
? `${df.ip ?? ''}${df.port ? `:${df.port}` : ''}`
|
||||
: '';
|
||||
return (
|
||||
<>
|
||||
{frontingTarget && (
|
||||
<div className="info-row">
|
||||
<dt>{t('pages.inbounds.form.mtgDomainFrontingIp')}</dt>
|
||||
<dd><Tag color="blue" className="value-tag">{frontingTarget}</Tag></dd>
|
||||
</div>
|
||||
)}
|
||||
{df?.proxyProtocol && (
|
||||
<div className="info-row">
|
||||
<dt>{t('pages.inbounds.form.mtgDomainFrontingProxyProtocol')}</dt>
|
||||
<dd><Tag color="green" className="value-tag">{t('enabled')}</Tag></dd>
|
||||
</div>
|
||||
)}
|
||||
{Boolean(s.proxyProtocolListener) && (
|
||||
<div className="info-row">
|
||||
<dt>{t('pages.inbounds.form.mtgProxyProtocolListener')}</dt>
|
||||
<dd><Tag color="green" className="value-tag">{t('enabled')}</Tag></dd>
|
||||
</div>
|
||||
)}
|
||||
{Boolean(s.preferIp) && (
|
||||
<div className="info-row">
|
||||
<dt>{t('pages.inbounds.form.mtgPreferIp')}</dt>
|
||||
<dd><Tag color="blue" className="value-tag">{s.preferIp as string}</Tag></dd>
|
||||
</div>
|
||||
)}
|
||||
{Boolean(s.debug) && (
|
||||
<div className="info-row">
|
||||
<dt>{t('pages.inbounds.form.mtgDebug')}</dt>
|
||||
<dd><Tag color="green" className="value-tag">{t('enabled')}</Tag></dd>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
{links.length > 0 && (
|
||||
<div className="info-row">
|
||||
<dt>{t('pages.inbounds.copyLink')}</dt>
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// mtg's [domain-fronting] section: where the sidecar forwards non-Telegram
|
||||
// traffic (e.g. an NGINX fake site). All optional — omitted keys fall back to
|
||||
// mtg's defaults (DNS-resolve the FakeTLS host, port 443, no proxy protocol).
|
||||
export const MtprotoDomainFrontingSchema = z.object({
|
||||
ip: z.string().optional(),
|
||||
port: z.number().int().min(0).max(65535).optional(),
|
||||
proxyProtocol: z.boolean().optional(),
|
||||
});
|
||||
export type MtprotoDomainFronting = z.infer<typeof MtprotoDomainFrontingSchema>;
|
||||
|
||||
// MTProto (Telegram) inbound. Served by an mtg sidecar process, not Xray, so
|
||||
// it has no clients and no stream settings. `secret` is the FakeTLS secret
|
||||
// (ee-prefixed); the backend rebuilds it to match `fakeTlsDomain` on save.
|
||||
// The remaining fields map to optional mtg config knobs and are written to the
|
||||
// generated mtg.toml only when set.
|
||||
export const MtprotoInboundSettingsSchema = z.object({
|
||||
fakeTlsDomain: z.string().default('www.cloudflare.com'),
|
||||
secret: z.string().default(''),
|
||||
proxyProtocolListener: z.boolean().optional(),
|
||||
preferIp: z.enum(['prefer-ipv6', 'prefer-ipv4', 'only-ipv6', 'only-ipv4']).optional(),
|
||||
debug: z.boolean().optional(),
|
||||
domainFronting: MtprotoDomainFrontingSchema.optional(),
|
||||
});
|
||||
export type MtprotoInboundSettings = z.infer<typeof MtprotoInboundSettingsSchema>;
|
||||
|
||||
@@ -672,6 +672,174 @@ exports[`protocol capability predicates > mtproto-basic :: xhttp/tls 1`] = `
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > mtproto-domain-fronting :: grpc/none 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
"isSS2022": false,
|
||||
"isSSMultiUser": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > mtproto-domain-fronting :: grpc/reality 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
"isSS2022": false,
|
||||
"isSSMultiUser": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > mtproto-domain-fronting :: grpc/tls 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
"isSS2022": false,
|
||||
"isSSMultiUser": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > mtproto-domain-fronting :: httpupgrade/none 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
"isSS2022": false,
|
||||
"isSSMultiUser": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > mtproto-domain-fronting :: httpupgrade/tls 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
"isSS2022": false,
|
||||
"isSSMultiUser": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > mtproto-domain-fronting :: kcp/none 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
"isSS2022": false,
|
||||
"isSSMultiUser": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > mtproto-domain-fronting :: tcp/none 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
"isSS2022": false,
|
||||
"isSSMultiUser": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > mtproto-domain-fronting :: tcp/reality 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
"isSS2022": false,
|
||||
"isSSMultiUser": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > mtproto-domain-fronting :: tcp/tls 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
"isSS2022": false,
|
||||
"isSSMultiUser": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > mtproto-domain-fronting :: ws/none 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
"isSS2022": false,
|
||||
"isSSMultiUser": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > mtproto-domain-fronting :: ws/tls 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
"isSS2022": false,
|
||||
"isSSMultiUser": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > mtproto-domain-fronting :: xhttp/none 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
"isSS2022": false,
|
||||
"isSSMultiUser": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > mtproto-domain-fronting :: xhttp/reality 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
"isSS2022": false,
|
||||
"isSSMultiUser": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > mtproto-domain-fronting :: xhttp/tls 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
"isSS2022": false,
|
||||
"isSSMultiUser": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`protocol capability predicates > shadowsocks-2022 :: grpc/none 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
|
||||
@@ -69,6 +69,24 @@ exports[`InboundSettingsSchema fixtures > parses mtproto-basic byte-stably 1`] =
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InboundSettingsSchema fixtures > parses mtproto-domain-fronting byte-stably 1`] = `
|
||||
{
|
||||
"protocol": "mtproto",
|
||||
"settings": {
|
||||
"debug": true,
|
||||
"domainFronting": {
|
||||
"ip": "127.0.0.1",
|
||||
"port": 9443,
|
||||
"proxyProtocol": true,
|
||||
},
|
||||
"fakeTlsDomain": "www.cloudflare.com",
|
||||
"preferIp": "prefer-ipv4",
|
||||
"proxyProtocolListener": true,
|
||||
"secret": "ee0123456789abcdef0123456789abcdef7777772e636c6f7564666c6172652e636f6d",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InboundSettingsSchema fixtures > parses shadowsocks-2022 byte-stably 1`] = `
|
||||
{
|
||||
"protocol": "shadowsocks",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"protocol": "mtproto",
|
||||
"settings": {
|
||||
"fakeTlsDomain": "www.cloudflare.com",
|
||||
"secret": "ee0123456789abcdef0123456789abcdef7777772e636c6f7564666c6172652e636f6d",
|
||||
"proxyProtocolListener": true,
|
||||
"preferIp": "prefer-ipv4",
|
||||
"debug": true,
|
||||
"domainFronting": {
|
||||
"ip": "127.0.0.1",
|
||||
"port": 9443,
|
||||
"proxyProtocol": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,6 +142,17 @@ describe('formValuesToWirePayload', () => {
|
||||
expect(payload.streamSettings).toBe('');
|
||||
});
|
||||
|
||||
it('emits empty sniffing for mtproto (mtg-served, not Xray)', () => {
|
||||
const values = rawInboundToFormValues({
|
||||
...vlessRow,
|
||||
protocol: 'mtproto',
|
||||
settings: { fakeTlsDomain: 'www.cloudflare.com', secret: 'ee00' },
|
||||
});
|
||||
const payload = formValuesToWirePayload(values);
|
||||
expect(payload.protocol).toBe('mtproto');
|
||||
expect(payload.sniffing).toBe('');
|
||||
});
|
||||
|
||||
it('omits nodeId when null', () => {
|
||||
const values = rawInboundToFormValues({ ...vlessRow, nodeId: null });
|
||||
const payload = formValuesToWirePayload(values);
|
||||
|
||||
+77
-12
@@ -23,6 +23,15 @@ type Instance struct {
|
||||
Listen string
|
||||
Port int
|
||||
Secret string
|
||||
|
||||
// Optional mtg tuning; each is omitted from the generated TOML when
|
||||
// zero-valued so mtg falls back to its own defaults.
|
||||
Debug bool
|
||||
ProxyProtocolListener bool
|
||||
PreferIP string
|
||||
FrontingIP string
|
||||
FrontingPort int
|
||||
FrontingProxyProtocol bool
|
||||
}
|
||||
|
||||
func (inst Instance) bindTo() string {
|
||||
@@ -33,8 +42,19 @@ func (inst Instance) bindTo() string {
|
||||
return fmt.Sprintf("%s:%d", listen, inst.Port)
|
||||
}
|
||||
|
||||
// fingerprint changes whenever any value that ends up in the generated TOML
|
||||
// changes, so ensureLocked restarts mtg when the operator edits a setting.
|
||||
func (inst Instance) fingerprint() string {
|
||||
return fmt.Sprintf("%s|%s", inst.bindTo(), inst.Secret)
|
||||
return strings.Join([]string{
|
||||
inst.bindTo(),
|
||||
inst.Secret,
|
||||
strconv.FormatBool(inst.Debug),
|
||||
strconv.FormatBool(inst.ProxyProtocolListener),
|
||||
inst.PreferIP,
|
||||
inst.FrontingIP,
|
||||
strconv.Itoa(inst.FrontingPort),
|
||||
strconv.FormatBool(inst.FrontingProxyProtocol),
|
||||
}, "|")
|
||||
}
|
||||
|
||||
// Traffic is a per-inbound traffic delta scraped from an mtg metrics endpoint.
|
||||
@@ -88,7 +108,15 @@ func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
|
||||
settings = healed
|
||||
}
|
||||
var parsed struct {
|
||||
Secret string `json:"secret"`
|
||||
Secret string `json:"secret"`
|
||||
Debug bool `json:"debug"`
|
||||
ProxyProtocolListener bool `json:"proxyProtocolListener"`
|
||||
PreferIP string `json:"preferIp"`
|
||||
DomainFronting struct {
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
ProxyProtocol bool `json:"proxyProtocol"`
|
||||
} `json:"domainFronting"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
|
||||
return Instance{}, false
|
||||
@@ -97,11 +125,17 @@ func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
|
||||
return Instance{}, false
|
||||
}
|
||||
return Instance{
|
||||
Id: ib.Id,
|
||||
Tag: ib.Tag,
|
||||
Listen: ib.Listen,
|
||||
Port: ib.Port,
|
||||
Secret: parsed.Secret,
|
||||
Id: ib.Id,
|
||||
Tag: ib.Tag,
|
||||
Listen: ib.Listen,
|
||||
Port: ib.Port,
|
||||
Secret: parsed.Secret,
|
||||
Debug: parsed.Debug,
|
||||
ProxyProtocolListener: parsed.ProxyProtocolListener,
|
||||
PreferIP: parsed.PreferIP,
|
||||
FrontingIP: parsed.DomainFronting.IP,
|
||||
FrontingPort: parsed.DomainFronting.Port,
|
||||
FrontingProxyProtocol: parsed.DomainFronting.ProxyProtocol,
|
||||
}, true
|
||||
}
|
||||
|
||||
@@ -143,7 +177,7 @@ func (m *Manager) ensureLocked(inst Instance) error {
|
||||
return err
|
||||
}
|
||||
cfgPath := configPathForID(inst.Id)
|
||||
if err := writeConfig(cfgPath, inst.Secret, inst.bindTo(), metricsPort); err != nil {
|
||||
if err := writeConfig(cfgPath, inst, metricsPort); err != nil {
|
||||
return err
|
||||
}
|
||||
proc := newProcess(cfgPath, fmt.Sprintf("inbound %d", inst.Id))
|
||||
@@ -282,13 +316,44 @@ func freeLocalPort() (int, error) {
|
||||
return l.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
|
||||
func writeConfig(path, secret, bindTo string, metricsPort int) error {
|
||||
// renderConfig builds the mtg TOML for an instance. Top-level keys must precede
|
||||
// any [section] header in TOML, so the layout is: required keys, then the
|
||||
// optional scalar tuning, then [domain-fronting], and finally [stats.prometheus]
|
||||
// — which x-ui always emits and scrapes for traffic (see scrapeTraffic).
|
||||
func renderConfig(inst Instance, metricsPort int) string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "secret = %q\n", inst.Secret)
|
||||
fmt.Fprintf(&b, "bind-to = %q\n", inst.bindTo())
|
||||
if inst.Debug {
|
||||
b.WriteString("debug = true\n")
|
||||
}
|
||||
if inst.ProxyProtocolListener {
|
||||
b.WriteString("proxy-protocol-listener = true\n")
|
||||
}
|
||||
if inst.PreferIP != "" {
|
||||
fmt.Fprintf(&b, "prefer-ip = %q\n", inst.PreferIP)
|
||||
}
|
||||
if inst.FrontingIP != "" || inst.FrontingPort > 0 || inst.FrontingProxyProtocol {
|
||||
b.WriteString("\n[domain-fronting]\n")
|
||||
if inst.FrontingIP != "" {
|
||||
fmt.Fprintf(&b, "ip = %q\n", inst.FrontingIP)
|
||||
}
|
||||
if inst.FrontingPort > 0 {
|
||||
fmt.Fprintf(&b, "port = %d\n", inst.FrontingPort)
|
||||
}
|
||||
if inst.FrontingProxyProtocol {
|
||||
b.WriteString("proxy-protocol = true\n")
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&b, "\n[stats.prometheus]\nenabled = true\nbind-to = \"127.0.0.1:%d\"\nhttp-path = \"/metrics\"\nmetric-prefix = \"mtg\"\n", metricsPort)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func writeConfig(path string, inst Instance, metricsPort int) error {
|
||||
if err := os.MkdirAll(configDir(), 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
content := fmt.Sprintf("secret = %q\nbind-to = %q\n\n[stats.prometheus]\nenabled = true\nbind-to = \"127.0.0.1:%d\"\nhttp-path = \"/metrics\"\nmetric-prefix = \"mtg\"\n",
|
||||
secret, bindTo, metricsPort)
|
||||
return os.WriteFile(path, []byte(content), 0o640)
|
||||
return os.WriteFile(path, []byte(renderConfig(inst, metricsPort)), 0o640)
|
||||
}
|
||||
|
||||
// scrapeTraffic reads the mtg Prometheus metrics endpoint and sums byte
|
||||
|
||||
+72
-1
@@ -1,6 +1,7 @@
|
||||
package mtproto
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
@@ -37,7 +38,9 @@ func TestInstanceFromInbound(t *testing.T) {
|
||||
Listen: "0.0.0.0",
|
||||
Port: 8443,
|
||||
Protocol: model.MTProto,
|
||||
Settings: `{"fakeTlsDomain":"example.com","secret":""}`,
|
||||
Settings: `{"fakeTlsDomain":"example.com","secret":"",` +
|
||||
`"debug":true,"proxyProtocolListener":true,"preferIp":"prefer-ipv4",` +
|
||||
`"domainFronting":{"ip":"127.0.0.1","port":9443,"proxyProtocol":true}}`,
|
||||
}
|
||||
inst, ok := InstanceFromInbound(ib)
|
||||
if !ok {
|
||||
@@ -49,8 +52,76 @@ func TestInstanceFromInbound(t *testing.T) {
|
||||
if inst.Port != 8443 || inst.Id != 3 {
|
||||
t.Fatalf("bad instance %+v", inst)
|
||||
}
|
||||
if !inst.Debug || !inst.ProxyProtocolListener || inst.PreferIP != "prefer-ipv4" {
|
||||
t.Fatalf("scalar options not parsed: %+v", inst)
|
||||
}
|
||||
if inst.FrontingIP != "127.0.0.1" || inst.FrontingPort != 9443 || !inst.FrontingProxyProtocol {
|
||||
t.Fatalf("domain-fronting not parsed: %+v", inst)
|
||||
}
|
||||
|
||||
if _, ok := InstanceFromInbound(&model.Inbound{Protocol: model.VLESS}); ok {
|
||||
t.Fatal("non-mtproto inbound should not produce an instance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderConfig(t *testing.T) {
|
||||
// A bare instance emits only the required keys and the prometheus block,
|
||||
// with no optional keys and no [domain-fronting] section.
|
||||
bare := renderConfig(Instance{Secret: "ee00", Listen: "0.0.0.0", Port: 8443}, 5000)
|
||||
for _, unwanted := range []string{"debug", "proxy-protocol-listener", "prefer-ip", "[domain-fronting]"} {
|
||||
if strings.Contains(bare, unwanted) {
|
||||
t.Fatalf("bare config should not contain %q:\n%s", unwanted, bare)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(bare, `bind-to = "0.0.0.0:8443"`) {
|
||||
t.Fatalf("missing bind-to:\n%s", bare)
|
||||
}
|
||||
if !strings.Contains(bare, "[stats.prometheus]") || !strings.Contains(bare, "127.0.0.1:5000") {
|
||||
t.Fatalf("prometheus block must always be present:\n%s", bare)
|
||||
}
|
||||
|
||||
// A fully configured instance emits every option and the fronting section.
|
||||
full := renderConfig(Instance{
|
||||
Secret: "ee11", Listen: "0.0.0.0", Port: 443,
|
||||
Debug: true, ProxyProtocolListener: true, PreferIP: "only-ipv6",
|
||||
FrontingIP: "127.0.0.1", FrontingPort: 9443, FrontingProxyProtocol: true,
|
||||
}, 6000)
|
||||
for _, want := range []string{
|
||||
"debug = true\n",
|
||||
"proxy-protocol-listener = true\n",
|
||||
`prefer-ip = "only-ipv6"`,
|
||||
"[domain-fronting]",
|
||||
`ip = "127.0.0.1"`,
|
||||
"port = 9443",
|
||||
"proxy-protocol = true\n",
|
||||
} {
|
||||
if !strings.Contains(full, want) {
|
||||
t.Fatalf("full config missing %q:\n%s", want, full)
|
||||
}
|
||||
}
|
||||
// TOML requires top-level keys before any [section] header.
|
||||
if strings.Index(full, "prefer-ip") > strings.Index(full, "[domain-fronting]") {
|
||||
t.Fatalf("top-level keys must precede the [domain-fronting] section:\n%s", full)
|
||||
}
|
||||
if strings.LastIndex(full, "[domain-fronting]") > strings.Index(full, "[stats.prometheus]") {
|
||||
t.Fatalf("[domain-fronting] must precede [stats.prometheus]:\n%s", full)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFingerprintReactsToOptions(t *testing.T) {
|
||||
base := Instance{Secret: "ee", Listen: "0.0.0.0", Port: 443}
|
||||
for name, mutate := range map[string]func(*Instance){
|
||||
"debug": func(i *Instance) { i.Debug = true },
|
||||
"listener": func(i *Instance) { i.ProxyProtocolListener = true },
|
||||
"preferIp": func(i *Instance) { i.PreferIP = "only-ipv4" },
|
||||
"frontingIP": func(i *Instance) { i.FrontingIP = "127.0.0.1" },
|
||||
"frontingPort": func(i *Instance) { i.FrontingPort = 9443 },
|
||||
"frontingProxy": func(i *Instance) { i.FrontingProxyProtocol = true },
|
||||
} {
|
||||
changed := base
|
||||
mutate(&changed)
|
||||
if base.fingerprint() == changed.fingerprint() {
|
||||
t.Fatalf("fingerprint must change when %s changes", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,7 +503,13 @@
|
||||
"encryptionMethod": "طريقة التشفير",
|
||||
"fakeTlsDomain": "نطاق FakeTLS (SNI)",
|
||||
"mtprotoSecret": "المفتاح السري",
|
||||
"mtprotoHint": "يتم تقديم MTProto عبر عملية mtg منفصلة وليس Xray. إعدادات النقل والعملاء لا تنطبق هنا — شارك الرابط أدناه مع تيليجرام.",
|
||||
"mtgDomainFrontingIp": "عنوان IP لـ Domain fronting",
|
||||
"mtgDomainFrontingPort": "منفذ Domain fronting",
|
||||
"mtgDomainFrontingProxyProtocol": "بروتوكول PROXY لـ Domain fronting",
|
||||
"mtgDomainFrontingHint": "المكان الذي يرسل إليه mtg حركة المرور غير الخاصة بتيليجرام — مثل موقع NGINX الوهمي لديك. اترك حقل IP فارغًا لاستخدام نطاق FakeTLS عبر DNS؛ المنفذ الافتراضي هو 443.",
|
||||
"mtgProxyProtocolListener": "قبول بروتوكول PROXY (المستمع)",
|
||||
"mtgPreferIp": "تفضيل IP",
|
||||
"mtgDebug": "سجل التصحيح",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "الإصدار",
|
||||
"udpIdleTimeout": "UDP idle timeout (ثانية)",
|
||||
|
||||
@@ -504,7 +504,13 @@
|
||||
"encryptionMethod": "Encryption method",
|
||||
"fakeTlsDomain": "FakeTLS domain (SNI)",
|
||||
"mtprotoSecret": "Secret",
|
||||
"mtprotoHint": "MTProto is served by a separate mtg process, not Xray. Stream settings and clients do not apply here — share the link below with Telegram.",
|
||||
"mtgDomainFrontingIp": "Domain fronting IP",
|
||||
"mtgDomainFrontingPort": "Domain fronting port",
|
||||
"mtgDomainFrontingProxyProtocol": "Domain fronting PROXY protocol",
|
||||
"mtgDomainFrontingHint": "Where mtg sends non-Telegram traffic — e.g. your NGINX fake site. Leave the IP empty to use the FakeTLS domain via DNS; default port is 443.",
|
||||
"mtgProxyProtocolListener": "Accept PROXY protocol (listener)",
|
||||
"mtgPreferIp": "IP preference",
|
||||
"mtgDebug": "Debug logging",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Version",
|
||||
"udpIdleTimeout": "UDP idle timeout (s)",
|
||||
|
||||
@@ -503,7 +503,13 @@
|
||||
"encryptionMethod": "Método de cifrado",
|
||||
"fakeTlsDomain": "Dominio FakeTLS (SNI)",
|
||||
"mtprotoSecret": "Secreto",
|
||||
"mtprotoHint": "MTProto se sirve mediante un proceso mtg independiente, no Xray. Los ajustes de transporte y los clientes no aplican aquí; comparte el enlace de abajo con Telegram.",
|
||||
"mtgDomainFrontingIp": "IP de domain fronting",
|
||||
"mtgDomainFrontingPort": "Puerto de domain fronting",
|
||||
"mtgDomainFrontingProxyProtocol": "Protocolo PROXY de domain fronting",
|
||||
"mtgDomainFrontingHint": "Adónde envía mtg el tráfico que no es de Telegram, p. ej. tu sitio web falso de NGINX. Deja la IP vacía para usar el dominio FakeTLS mediante DNS; el puerto predeterminado es 443.",
|
||||
"mtgProxyProtocolListener": "Aceptar protocolo PROXY (escucha)",
|
||||
"mtgPreferIp": "Preferencia de IP",
|
||||
"mtgDebug": "Registro de depuración",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Versión",
|
||||
"udpIdleTimeout": "UDP idle timeout (s)",
|
||||
|
||||
@@ -503,7 +503,13 @@
|
||||
"encryptionMethod": "روش رمزنگاری",
|
||||
"fakeTlsDomain": "دامنه FakeTLS (SNI)",
|
||||
"mtprotoSecret": "کلید مخفی",
|
||||
"mtprotoHint": "پروتکل MTProto توسط یک پردازش جداگانه mtg ارائه میشود، نه Xray. تنظیمات انتقال و کلاینتها اینجا کاربرد ندارند — لینک زیر را با تلگرام به اشتراک بگذارید.",
|
||||
"mtgDomainFrontingIp": "آیپی Domain fronting",
|
||||
"mtgDomainFrontingPort": "پورت Domain fronting",
|
||||
"mtgDomainFrontingProxyProtocol": "پروتکل PROXY برای Domain fronting",
|
||||
"mtgDomainFrontingHint": "جایی که mtg ترافیک غیرتلگرامی را به آن ارسال میکند — مثلاً سایت جعلی NGINX شما. برای استفاده از دامنهٔ FakeTLS از طریق DNS، فیلد IP را خالی بگذارید؛ پورت پیشفرض 443 است.",
|
||||
"mtgProxyProtocolListener": "پذیرش پروتکل PROXY (شنونده)",
|
||||
"mtgPreferIp": "ترجیح IP",
|
||||
"mtgDebug": "گزارش اشکالزدایی",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "نسخه",
|
||||
"udpIdleTimeout": "UDP idle timeout (s)",
|
||||
|
||||
@@ -503,7 +503,13 @@
|
||||
"encryptionMethod": "Metode enkripsi",
|
||||
"fakeTlsDomain": "Domain FakeTLS (SNI)",
|
||||
"mtprotoSecret": "Secret",
|
||||
"mtprotoHint": "MTProto dijalankan oleh proses mtg terpisah, bukan Xray. Pengaturan stream dan klien tidak berlaku di sini — bagikan tautan di bawah ke Telegram.",
|
||||
"mtgDomainFrontingIp": "IP domain fronting",
|
||||
"mtgDomainFrontingPort": "Port domain fronting",
|
||||
"mtgDomainFrontingProxyProtocol": "Protokol PROXY domain fronting",
|
||||
"mtgDomainFrontingHint": "Tujuan mtg mengirim lalu lintas non-Telegram — mis. situs palsu NGINX Anda. Kosongkan IP untuk memakai domain FakeTLS melalui DNS; port bawaan adalah 443.",
|
||||
"mtgProxyProtocolListener": "Terima protokol PROXY (listener)",
|
||||
"mtgPreferIp": "Preferensi IP",
|
||||
"mtgDebug": "Log debug",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Versi",
|
||||
"udpIdleTimeout": "UDP idle timeout (d)",
|
||||
|
||||
@@ -503,7 +503,13 @@
|
||||
"encryptionMethod": "暗号化方式",
|
||||
"fakeTlsDomain": "FakeTLS ドメイン (SNI)",
|
||||
"mtprotoSecret": "シークレット",
|
||||
"mtprotoHint": "MTProto は Xray ではなく独立した mtg プロセスで提供されます。ストリーム設定とクライアントはここでは適用されません。下のリンクを Telegram で共有してください。",
|
||||
"mtgDomainFrontingIp": "ドメインフロンティング IP",
|
||||
"mtgDomainFrontingPort": "ドメインフロンティング ポート",
|
||||
"mtgDomainFrontingProxyProtocol": "ドメインフロンティング PROXY プロトコル",
|
||||
"mtgDomainFrontingHint": "mtg が Telegram 以外のトラフィックを転送する先 — 例: あなたの NGINX ダミーサイト。IP を空欄にすると DNS 経由で FakeTLS ドメインを使用します。デフォルトポートは 443 です。",
|
||||
"mtgProxyProtocolListener": "PROXY プロトコルを受け入れる(リスナー)",
|
||||
"mtgPreferIp": "IP の優先設定",
|
||||
"mtgDebug": "デバッグログ",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "バージョン",
|
||||
"udpIdleTimeout": "UDP idle timeout (秒)",
|
||||
|
||||
@@ -503,7 +503,13 @@
|
||||
"encryptionMethod": "Método de criptografia",
|
||||
"fakeTlsDomain": "Domínio FakeTLS (SNI)",
|
||||
"mtprotoSecret": "Segredo",
|
||||
"mtprotoHint": "O MTProto é servido por um processo mtg separado, não pelo Xray. As configurações de transporte e os clientes não se aplicam aqui — compartilhe o link abaixo com o Telegram.",
|
||||
"mtgDomainFrontingIp": "IP de domain fronting",
|
||||
"mtgDomainFrontingPort": "Porta de domain fronting",
|
||||
"mtgDomainFrontingProxyProtocol": "Protocolo PROXY de domain fronting",
|
||||
"mtgDomainFrontingHint": "Para onde o mtg envia o tráfego que não é do Telegram — por exemplo, seu site falso NGINX. Deixe o IP vazio para usar o domínio FakeTLS via DNS; a porta padrão é 443.",
|
||||
"mtgProxyProtocolListener": "Aceitar protocolo PROXY (listener)",
|
||||
"mtgPreferIp": "Preferência de IP",
|
||||
"mtgDebug": "Log de depuração",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Versão",
|
||||
"udpIdleTimeout": "UDP idle timeout (s)",
|
||||
|
||||
@@ -503,7 +503,13 @@
|
||||
"encryptionMethod": "Метод шифрования",
|
||||
"fakeTlsDomain": "Домен FakeTLS (SNI)",
|
||||
"mtprotoSecret": "Секрет",
|
||||
"mtprotoHint": "MTProto обслуживается отдельным процессом mtg, а не Xray. Настройки транспорта и клиенты здесь не применяются — поделитесь ссылкой ниже в Telegram.",
|
||||
"mtgDomainFrontingIp": "IP домен-фронтинга",
|
||||
"mtgDomainFrontingPort": "Порт домен-фронтинга",
|
||||
"mtgDomainFrontingProxyProtocol": "PROXY-протокол домен-фронтинга",
|
||||
"mtgDomainFrontingHint": "Куда mtg отправляет не-Telegram трафик — например, на ваш фейковый сайт NGINX. Оставьте IP пустым, чтобы использовать домен FakeTLS через DNS; порт по умолчанию — 443.",
|
||||
"mtgProxyProtocolListener": "Принимать PROXY-протокол (слушатель)",
|
||||
"mtgPreferIp": "Предпочтение IP",
|
||||
"mtgDebug": "Журнал отладки",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Версия",
|
||||
"udpIdleTimeout": "UDP idle timeout (с)",
|
||||
|
||||
@@ -504,7 +504,13 @@
|
||||
"encryptionMethod": "Şifreleme Yöntemi",
|
||||
"fakeTlsDomain": "FakeTLS alan adı (SNI)",
|
||||
"mtprotoSecret": "Gizli Anahtar (Secret)",
|
||||
"mtprotoHint": "MTProto, Xray tarafından değil, ayrı bir mtg işlemi tarafından sunulur. Akış ayarları ve kullanıcılar burada geçerli değildir — aşağıdaki bağlantıyı Telegram ile paylaşın.",
|
||||
"mtgDomainFrontingIp": "Domain fronting IP",
|
||||
"mtgDomainFrontingPort": "Domain fronting portu",
|
||||
"mtgDomainFrontingProxyProtocol": "Domain fronting PROXY protokolü",
|
||||
"mtgDomainFrontingHint": "mtg'nin Telegram dışı trafiği gönderdiği yer — örn. NGINX sahte siteniz. FakeTLS alan adını DNS üzerinden kullanmak için IP'yi boş bırakın; varsayılan port 443'tür.",
|
||||
"mtgProxyProtocolListener": "PROXY protokolünü kabul et (dinleyici)",
|
||||
"mtgPreferIp": "IP tercihi",
|
||||
"mtgDebug": "Hata ayıklama günlüğü",
|
||||
"visionTestseed": "Vision Testseed",
|
||||
"version": "Sürüm",
|
||||
"udpIdleTimeout": "UDP Idle Timeout (s)",
|
||||
|
||||
@@ -503,7 +503,13 @@
|
||||
"encryptionMethod": "Метод шифрування",
|
||||
"fakeTlsDomain": "Домен FakeTLS (SNI)",
|
||||
"mtprotoSecret": "Секрет",
|
||||
"mtprotoHint": "MTProto обслуговується окремим процесом mtg, а не Xray. Налаштування транспорту та клієнти тут не застосовуються — поділіться посиланням нижче в Telegram.",
|
||||
"mtgDomainFrontingIp": "IP домен-фронтингу",
|
||||
"mtgDomainFrontingPort": "Порт домен-фронтингу",
|
||||
"mtgDomainFrontingProxyProtocol": "PROXY-протокол домен-фронтингу",
|
||||
"mtgDomainFrontingHint": "Куди mtg надсилає не-Telegram трафік — наприклад, на ваш фейковий сайт NGINX. Залиште IP порожнім, щоб використовувати домен FakeTLS через DNS; типовий порт — 443.",
|
||||
"mtgProxyProtocolListener": "Приймати PROXY-протокол (слухач)",
|
||||
"mtgPreferIp": "Перевага IP",
|
||||
"mtgDebug": "Журнал налагодження",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Версія",
|
||||
"udpIdleTimeout": "UDP idle timeout (с)",
|
||||
|
||||
@@ -503,7 +503,13 @@
|
||||
"encryptionMethod": "Phương thức mã hóa",
|
||||
"fakeTlsDomain": "Tên miền FakeTLS (SNI)",
|
||||
"mtprotoSecret": "Khóa bí mật",
|
||||
"mtprotoHint": "MTProto được phục vụ bởi một tiến trình mtg riêng, không phải Xray. Cài đặt truyền tải và máy khách không áp dụng ở đây — hãy chia sẻ liên kết bên dưới với Telegram.",
|
||||
"mtgDomainFrontingIp": "IP domain fronting",
|
||||
"mtgDomainFrontingPort": "Cổng domain fronting",
|
||||
"mtgDomainFrontingProxyProtocol": "Giao thức PROXY domain fronting",
|
||||
"mtgDomainFrontingHint": "Nơi mtg gửi lưu lượng không phải Telegram — ví dụ trang web giả NGINX của bạn. Để trống IP để dùng tên miền FakeTLS qua DNS; cổng mặc định là 443.",
|
||||
"mtgProxyProtocolListener": "Chấp nhận giao thức PROXY (trình lắng nghe)",
|
||||
"mtgPreferIp": "Ưu tiên IP",
|
||||
"mtgDebug": "Nhật ký gỡ lỗi",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Phiên bản",
|
||||
"udpIdleTimeout": "UDP idle timeout (s)",
|
||||
|
||||
@@ -503,7 +503,13 @@
|
||||
"encryptionMethod": "加密方法",
|
||||
"fakeTlsDomain": "FakeTLS 域名 (SNI)",
|
||||
"mtprotoSecret": "密钥",
|
||||
"mtprotoHint": "MTProto 由独立的 mtg 进程提供服务,而非 Xray。传输设置和客户端在此不适用——请将下方链接分享到 Telegram。",
|
||||
"mtgDomainFrontingIp": "域前置 IP",
|
||||
"mtgDomainFrontingPort": "域前置端口",
|
||||
"mtgDomainFrontingProxyProtocol": "域前置 PROXY 协议",
|
||||
"mtgDomainFrontingHint": "mtg 转发非 Telegram 流量的目标——例如你的 NGINX 伪装站点。留空 IP 则通过 DNS 解析 FakeTLS 域名;默认端口为 443。",
|
||||
"mtgProxyProtocolListener": "接受 PROXY 协议(监听器)",
|
||||
"mtgPreferIp": "IP 优先级",
|
||||
"mtgDebug": "调试日志",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "版本",
|
||||
"udpIdleTimeout": "UDP 空闲超时 (s)",
|
||||
|
||||
@@ -503,7 +503,13 @@
|
||||
"encryptionMethod": "加密方法",
|
||||
"fakeTlsDomain": "FakeTLS 網域 (SNI)",
|
||||
"mtprotoSecret": "金鑰",
|
||||
"mtprotoHint": "MTProto 由獨立的 mtg 程序提供服務,而非 Xray。傳輸設定與用戶端在此不適用——請將下方連結分享至 Telegram。",
|
||||
"mtgDomainFrontingIp": "網域前置 IP",
|
||||
"mtgDomainFrontingPort": "網域前置連接埠",
|
||||
"mtgDomainFrontingProxyProtocol": "網域前置 PROXY 協定",
|
||||
"mtgDomainFrontingHint": "mtg 轉發非 Telegram 流量的目標——例如你的 NGINX 偽裝站台。IP 留空則透過 DNS 解析 FakeTLS 網域;預設連接埠為 443。",
|
||||
"mtgProxyProtocolListener": "接受 PROXY 協定(監聽器)",
|
||||
"mtgPreferIp": "IP 偏好",
|
||||
"mtgDebug": "除錯日誌",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "版本",
|
||||
"udpIdleTimeout": "UDP 閒置逾時 (s)",
|
||||
|
||||
Reference in New Issue
Block a user