Files
3x-ui/frontend/src/pages/inbounds/form/protocols/mtproto.tsx
T
MHSanaei 5eec178483 feat(mtproto): route Telegram egress through Xray routing rules
Add a per-inbound "Route through Xray" toggle (off by default) plus an
optional outbound picker on MTProto inbounds. mtg only supports a SOCKS5
upstream, so when enabled the panel injects a loopback SOCKS bridge into
the generated Xray config — tagged with the inbound's own tag — and mtg
dials Telegram through it via a [network] proxies upstream. The router
then governs Telegram egress: matchable in the Routing tab, or forced to a
chosen outbound/balancer via the picker.

- mtproto: Instance carries RouteThroughXray + XrayRoutePort (in the
  fingerprint); InstanceFromInbound parses them; renderConfig emits the
  socks5 [network] upstream; freeLocalPort exported as FreeLocalPort.
- xray.go: injectMtprotoEgress appends the loopback SOCKS bridge and
  prepends an optional inboundTag->outbound/balancer rule, hot-appliable
  like injectPanelEgress.
- inbound.go: backend-owned egress port persisted in settings, allocated
  once and carried across edits (stored value wins); stripped with the
  inert outboundTag when routing is off; allocation failure fails the save;
  routed add/update/del force a config regen.
- mtproto_job: skip folding mtg metrics for routed inbounds (the bridge,
  carrying the inbound tag, is metered by xray_traffic_job) to avoid
  double-counting.
- frontend: toggle + outbound/balancer Select (useOutboundTags) on the
  MTProto form; i18n keys for all locales.
2026-06-12 17:58:45 +02:00

102 lines
3.9 KiB
TypeScript

import { useTranslation } from 'react-i18next';
import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import { generateMtprotoSecret, mtprotoSecretForDomain } from '@/lib/xray/inbound-defaults';
import { useOutboundTags } from '@/api/queries/useOutboundTags';
export default function MtprotoFields() {
const { t } = useTranslation();
const form = Form.useFormInstance();
const routeThroughXray = Form.useWatch(['settings', 'routeThroughXray'], form) as boolean | undefined;
const { data: outboundTags } = useOutboundTags();
return (
<>
<Form.Item name={['settings', 'fakeTlsDomain']} label={t('pages.inbounds.form.fakeTlsDomain')}>
<Input
placeholder="www.cloudflare.com"
onChange={(e) => {
const current = (form.getFieldValue(['settings', 'secret']) as string) ?? '';
form.setFieldValue(['settings', 'secret'], mtprotoSecretForDomain(current, e.target.value));
}}
/>
</Form.Item>
<Form.Item label={t('pages.inbounds.form.mtprotoSecret')}>
<Space.Compact block>
<Form.Item name={['settings', 'secret']} noStyle>
<Input readOnly style={{ width: 'calc(100% - 32px)' }} />
</Form.Item>
<Button
icon={<ReloadOutlined />}
onClick={() => {
const domain = form.getFieldValue(['settings', 'fakeTlsDomain']);
form.setFieldValue(['settings', 'secret'], generateMtprotoSecret(domain as string));
}}
/>
</Space.Compact>
</Form.Item>
<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>
<Form.Item
name={['settings', 'routeThroughXray']}
label={t('pages.inbounds.form.mtgRouteThroughXray')}
tooltip={t('pages.inbounds.form.mtgRouteThroughXrayHint')}
valuePropName="checked"
>
<Switch />
</Form.Item>
{routeThroughXray && (
<Form.Item
name={['settings', 'outboundTag']}
label={t('pages.inbounds.form.mtgRouteOutbound')}
tooltip={t('pages.inbounds.form.mtgRouteOutboundHint')}
>
<Select
allowClear
showSearch
placeholder={t('pages.inbounds.form.mtgRouteOutboundPlaceholder')}
options={(outboundTags ?? []).map((tag) => ({ value: tag, label: tag }))}
/>
</Form.Item>
)}
</>
);
}