mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-17 10:06:08 +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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user