From 1ca5924a446d3bafb97e3eeea3074c4e0ee039ee Mon Sep 17 00:00:00 2001 From: Sanaei Date: Mon, 8 Jun 2026 14:28:19 +0200 Subject: [PATCH] feat(mtproto): add MTProto (FakeTLS) protocol via managed mtg sidecar (#5076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(mtproto): add MTProto (FakeTLS) protocol via managed mtg sidecar Xray-core has no mtproto proxy, so mtproto inbounds run as standalone mtg (9seconds/mtg) sidecar processes managed by the panel — one per inbound — and are excluded from the generated Xray config entirely. - model: MTProto protocol constant, validator, and FakeTLS secret helpers (GenerateFakeTLSSecret/HealMtprotoSecret) - mtproto package: per-inbound mtg process manager with reconcile, graceful stop, and best-effort Prometheus traffic scraping - runtime: delegate mtproto inbounds to the mtg manager instead of the Xray gRPC API; skip mtproto when building the Xray config - web: boot reconcile + StopAll wiring, periodic reconcile/traffic job, port-conflict transport, secret healing on inbound add/update - sub: tg:// proxy share-link generation - frontend: protocol option, Zod schema, Protocol tab (FakeTLS domain + regenerable secret), info-modal link, and i18n - provisioning: fetch mtg v2.2.8 in install.sh, DockerInit.sh, and the Linux + Windows release workflows * fix * fix * fix: address Copilot review comments on mtproto PR - web/web.go: create NewMtprotoJob once and reuse for cron + initial run - mtproto/manager.go: StopAll cleans up per-inbound config files on shutdown - mtproto/manager.go: CollectTraffic releases mutex before HTTP scrapes to avoid blocking Ensure/Reconcile/Remove during network I/O - database/model/model.go: panic on crypto/rand failure in mtprotoRandomMiddle instead of silently producing a weak all-zero secret - install.sh: fix chmod to handle renamed bin/mtg-linux-arm on armv5/v6/v7 --- .github/workflows/release.yml | 19 + DockerInit.sh | 12 + database/model/model.go | 69 +++- database/model/model_mtproto_test.go | 71 ++++ frontend/public/openapi.json | 3 +- frontend/src/generated/schemas.ts | 3 +- frontend/src/generated/zod.ts | 2 +- frontend/src/lib/xray/inbound-defaults.ts | 42 ++- frontend/src/lib/xray/inbound-link.ts | 24 ++ .../pages/inbounds/form/InboundFormModal.tsx | 4 + .../pages/inbounds/form/protocols/index.ts | 1 + .../pages/inbounds/form/protocols/mtproto.tsx | 40 ++ .../pages/inbounds/info/InboundInfoModal.tsx | 29 ++ frontend/src/schemas/primitives/protocol.ts | 2 + .../src/schemas/protocols/inbound/index.ts | 3 + .../src/schemas/protocols/inbound/mtproto.ts | 10 + .../protocol-capabilities.test.ts.snap | 168 +++++++++ .../test/__snapshots__/protocols.test.ts.snap | 10 + .../fixtures/inbound/mtproto-basic.json | 7 + install.sh | 9 + mtproto/manager.go | 348 ++++++++++++++++++ mtproto/manager_test.go | 56 +++ mtproto/process.go | 201 ++++++++++ mtproto/process_other.go | 7 + mtproto/process_windows.go | 66 ++++ sub/subService.go | 35 +- web/job/mtproto_job.go | 62 ++++ web/runtime/local.go | 18 + web/service/api_scale_postgres_test.go | 5 +- web/service/inbound.go | 13 + web/service/port_conflict.go | 2 + web/service/xray.go | 3 + web/translation/ar-EG.json | 3 + web/translation/en-US.json | 3 + web/translation/es-ES.json | 3 + web/translation/fa-IR.json | 3 + web/translation/id-ID.json | 3 + web/translation/ja-JP.json | 3 + web/translation/pt-BR.json | 3 + web/translation/ru-RU.json | 3 + web/translation/tr-TR.json | 3 + web/translation/uk-UA.json | 3 + web/translation/vi-VN.json | 3 + web/translation/zh-CN.json | 3 + web/translation/zh-TW.json | 3 + web/web.go | 7 + 46 files changed, 1381 insertions(+), 9 deletions(-) create mode 100644 database/model/model_mtproto_test.go create mode 100644 frontend/src/pages/inbounds/form/protocols/mtproto.tsx create mode 100644 frontend/src/schemas/protocols/inbound/mtproto.ts create mode 100644 frontend/src/test/golden/fixtures/inbound/mtproto-basic.json create mode 100644 mtproto/manager.go create mode 100644 mtproto/manager_test.go create mode 100644 mtproto/process.go create mode 100644 mtproto/process_other.go create mode 100644 mtproto/process_windows.go create mode 100644 web/job/mtproto_job.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6fa0cb5c..4cc049c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -150,6 +150,16 @@ jobs: wget -q -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat wget -q -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat mv xray xray-linux-${{ matrix.platform }} + # mtg (MTProto sidecar) - only for arches mtg publishes + MTG_VER="2.2.8" + case "${{ matrix.platform }}" in + amd64|arm64|armv7|armv6|386) + wget -q "https://github.com/9seconds/mtg/releases/download/v${MTG_VER}/mtg-${MTG_VER}-linux-${{ matrix.platform }}.tar.gz" + tar -xzf "mtg-${MTG_VER}-linux-${{ matrix.platform }}.tar.gz" + mv "mtg-${MTG_VER}-linux-${{ matrix.platform }}/mtg" "mtg-linux-${{ matrix.platform }}" 2>/dev/null || mv mtg "mtg-linux-${{ matrix.platform }}" + rm -rf "mtg-${MTG_VER}-linux-${{ matrix.platform }}" "mtg-${MTG_VER}-linux-${{ matrix.platform }}.tar.gz" + ;; + esac cd ../.. - name: Package @@ -258,6 +268,15 @@ jobs: Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat" Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat" Rename-Item xray.exe xray-windows-amd64.exe + + # Download mtg (MTProto sidecar) for Windows + $MTG_VER = "2.2.8" + Invoke-WebRequest -Uri "https://github.com/9seconds/mtg/releases/download/v$MTG_VER/mtg-$MTG_VER-windows-amd64.zip" -OutFile "mtg-windows-amd64.zip" + Expand-Archive -Path "mtg-windows-amd64.zip" -DestinationPath "mtg-tmp" + $mtgExe = Get-ChildItem -Path "mtg-tmp" -Recurse -Filter "mtg.exe" | Select-Object -First 1 + Move-Item $mtgExe.FullName "mtg-windows-amd64.exe" + Remove-Item "mtg-windows-amd64.zip", "mtg-tmp" -Recurse -Force + cd .. Copy-Item -Path ..\windows_files\* -Destination . -Recurse cd .. diff --git a/DockerInit.sh b/DockerInit.sh index 3e9b811e..0ae45721 100755 --- a/DockerInit.sh +++ b/DockerInit.sh @@ -3,34 +3,46 @@ case $1 in amd64) ARCH="64" FNAME="amd64" + MTG_ARCH="amd64" ;; i386) ARCH="32" FNAME="i386" + MTG_ARCH="386" ;; armv8 | arm64 | aarch64) ARCH="arm64-v8a" FNAME="arm64" + MTG_ARCH="arm64" ;; armv7 | arm | arm32) ARCH="arm32-v7a" FNAME="arm32" + MTG_ARCH="armv7" ;; armv6) ARCH="arm32-v6" FNAME="armv6" + MTG_ARCH="armv6" ;; *) ARCH="64" FNAME="amd64" + MTG_ARCH="amd64" ;; esac +MTG_VER="2.2.8" mkdir -p build/bin cd build/bin curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v26.6.1/Xray-linux-${ARCH}.zip" unzip "Xray-linux-${ARCH}.zip" rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat mv xray "xray-linux-${FNAME}" +curl -sfLRO "https://github.com/9seconds/mtg/releases/download/v${MTG_VER}/mtg-${MTG_VER}-linux-${MTG_ARCH}.tar.gz" +tar -xzf "mtg-${MTG_VER}-linux-${MTG_ARCH}.tar.gz" +mv "mtg-${MTG_VER}-linux-${MTG_ARCH}/mtg" "mtg-linux-${FNAME}" 2>/dev/null || mv mtg "mtg-linux-${FNAME}" +rm -rf "mtg-${MTG_VER}-linux-${MTG_ARCH}" "mtg-${MTG_VER}-linux-${MTG_ARCH}.tar.gz" +chmod +x "mtg-linux-${FNAME}" curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat curl -sfLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat diff --git a/database/model/model.go b/database/model/model.go index ddb516b1..4077995e 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -3,6 +3,8 @@ package model import ( "bytes" + "crypto/rand" + "encoding/hex" "encoding/json" "fmt" "strings" @@ -29,6 +31,7 @@ const ( Mixed Protocol = "mixed" WireGuard Protocol = "wireguard" Hysteria Protocol = "hysteria" + MTProto Protocol = "mtproto" ) // User represents a user account in the 3x-ui panel. @@ -56,7 +59,7 @@ type Inbound struct { // Xray configuration fields Listen string `json:"listen" form:"listen"` Port int `json:"port" form:"port" validate:"gte=0,lte=65535" example:"443"` - Protocol Protocol `json:"protocol" form:"protocol" validate:"required,oneof=vmess vless trojan shadowsocks wireguard hysteria http mixed tunnel tun" example:"vless"` + Protocol Protocol `json:"protocol" form:"protocol" validate:"required,oneof=vmess vless trojan shadowsocks wireguard hysteria http mixed tunnel tun mtproto" example:"vless"` Settings string `json:"settings" form:"settings"` StreamSettings string `json:"streamSettings" form:"streamSettings"` Tag string `json:"tag" form:"tag" gorm:"unique" example:"in-443-tcp"` @@ -366,6 +369,70 @@ func HealShadowsocksClientMethods(settings string) (string, bool) { return string(out), true } +// GenerateFakeTLSSecret builds an MTProto FakeTLS secret for the given domain: +// the "ee" FakeTLS marker, 16 random bytes, then the domain encoded as hex. +// This single value is what mtg's config and the client tg:// link both use. +func GenerateFakeTLSSecret(domain string) string { + return "ee" + mtprotoRandomMiddle() + hex.EncodeToString([]byte(domain)) +} + +func mtprotoRandomMiddle() string { + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + panic(fmt.Errorf("mtproto: crypto/rand read failed: %w", err)) + } + return hex.EncodeToString(buf) +} + +// mtprotoSecretMiddle returns the 16-byte random middle of an existing secret +// when it is well-formed, otherwise a freshly generated one. Reusing the middle +// keeps the secret stable when only the FakeTLS domain changes. +func mtprotoSecretMiddle(secret string) string { + s := secret + if strings.HasPrefix(s, "ee") || strings.HasPrefix(s, "dd") { + s = s[2:] + } + if len(s) >= 32 { + mid := s[:32] + if _, err := hex.DecodeString(mid); err == nil { + return mid + } + } + return mtprotoRandomMiddle() +} + +// HealMtprotoSecret normalises an mtproto inbound's settings JSON before the +// value leaves for the mtg sidecar or a share link: it rebuilds `secret` so it +// is always a valid FakeTLS secret whose trailing domain matches +// `fakeTlsDomain`, generating the random middle when one is missing and +// rewriting the domain suffix when the domain changed. Returns the rewritten +// settings and true when anything changed. +func HealMtprotoSecret(settings string) (string, bool) { + if settings == "" { + return settings, false + } + var parsed map[string]any + if err := json.Unmarshal([]byte(settings), &parsed); err != nil { + return settings, false + } + domain, _ := parsed["fakeTlsDomain"].(string) + domain = strings.TrimSpace(domain) + if domain == "" { + return settings, false + } + secret, _ := parsed["secret"].(string) + expected := "ee" + mtprotoSecretMiddle(secret) + hex.EncodeToString([]byte(domain)) + if secret == expected { + return settings, false + } + parsed["secret"] = expected + out, err := json.MarshalIndent(parsed, "", " ") + if err != nil { + return settings, false + } + return string(out), true +} + // Setting stores key-value configuration settings for the 3x-ui panel. type Setting struct { Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` diff --git a/database/model/model_mtproto_test.go b/database/model/model_mtproto_test.go new file mode 100644 index 00000000..813a2d11 --- /dev/null +++ b/database/model/model_mtproto_test.go @@ -0,0 +1,71 @@ +package model + +import ( + "encoding/hex" + "encoding/json" + "strings" + "testing" +) + +func TestGenerateFakeTLSSecret(t *testing.T) { + domain := "www.cloudflare.com" + s := GenerateFakeTLSSecret(domain) + if !strings.HasPrefix(s, "ee") { + t.Fatalf("secret must start with ee, got %q", s) + } + wantSuffix := hex.EncodeToString([]byte(domain)) + if !strings.HasSuffix(s, wantSuffix) { + t.Fatalf("secret must end with hex(domain) %q, got %q", wantSuffix, s) + } + if len(s) != 2+32+len(wantSuffix) { + t.Fatalf("unexpected secret length %d", len(s)) + } + if _, err := hex.DecodeString(s[2:34]); err != nil { + t.Fatalf("middle is not valid hex: %v", err) + } +} + +func TestHealMtprotoSecret(t *testing.T) { + domain := "example.com" + suffix := hex.EncodeToString([]byte(domain)) + + in := `{"fakeTlsDomain":"example.com","secret":""}` + out, changed := HealMtprotoSecret(in) + if !changed { + t.Fatal("expected heal to populate an empty secret") + } + var parsed map[string]any + if err := json.Unmarshal([]byte(out), &parsed); err != nil { + t.Fatalf("healed settings not valid json: %v", err) + } + got, _ := parsed["secret"].(string) + if !strings.HasPrefix(got, "ee") || !strings.HasSuffix(got, suffix) { + t.Fatalf("healed secret malformed: %q", got) + } + + if _, changed2 := HealMtprotoSecret(out); changed2 { + t.Fatal("expected no change for an already-valid secret") + } + + mid := got[2:34] + newDomain := "telegram.org" + in3 := `{"fakeTlsDomain":"telegram.org","secret":"` + got + `"}` + out3, changed3 := HealMtprotoSecret(in3) + if !changed3 { + t.Fatal("expected heal to rewrite the domain suffix") + } + if err := json.Unmarshal([]byte(out3), &parsed); err != nil { + t.Fatalf("healed settings not valid json: %v", err) + } + got3, _ := parsed["secret"].(string) + if got3[2:34] != mid { + t.Fatalf("random middle should be preserved on domain change: %q vs %q", got3[2:34], mid) + } + if !strings.HasSuffix(got3, hex.EncodeToString([]byte(newDomain))) { + t.Fatalf("suffix not updated for new domain: %q", got3) + } + + if _, changed4 := HealMtprotoSecret(`{"secret":"ee"}`); changed4 { + t.Fatal("expected no change when fakeTlsDomain is missing") + } +} diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 259050a4..3ec26788 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -1341,7 +1341,8 @@ "http", "mixed", "tunnel", - "tun" + "tun", + "mtproto" ], "example": "vless", "type": "string" diff --git a/frontend/src/generated/schemas.ts b/frontend/src/generated/schemas.ts index bc4240fe..00471dc0 100644 --- a/frontend/src/generated/schemas.ts +++ b/frontend/src/generated/schemas.ts @@ -1315,7 +1315,8 @@ export const SCHEMAS: Record = { "http", "mixed", "tunnel", - "tun" + "tun", + "mtproto" ], "example": "vless", "type": "string" diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index 5b256b73..14c1f909 100644 --- a/frontend/src/generated/zod.ts +++ b/frontend/src/generated/zod.ts @@ -316,7 +316,7 @@ export const InboundSchema = z.object({ nodeId: z.number().int().nullable().optional(), originNodeGuid: z.string().optional(), port: z.number().int().min(0).max(65535), - protocol: z.enum(['vmess', 'vless', 'trojan', 'shadowsocks', 'wireguard', 'hysteria', 'http', 'mixed', 'tunnel', 'tun']), + protocol: z.enum(['vmess', 'vless', 'trojan', 'shadowsocks', 'wireguard', 'hysteria', 'http', 'mixed', 'tunnel', 'tun', 'mtproto']), remark: z.string(), settings: z.unknown(), sniffing: z.unknown(), diff --git a/frontend/src/lib/xray/inbound-defaults.ts b/frontend/src/lib/xray/inbound-defaults.ts index 039ff730..37912f0e 100644 --- a/frontend/src/lib/xray/inbound-defaults.ts +++ b/frontend/src/lib/xray/inbound-defaults.ts @@ -3,6 +3,7 @@ import { RandomUtil, Wireguard } from '@/utils'; import type { HttpInboundSettings } from '@/schemas/protocols/inbound/http'; import type { HysteriaClient, HysteriaInboundSettings } from '@/schemas/protocols/inbound/hysteria'; import type { MixedInboundSettings } from '@/schemas/protocols/inbound/mixed'; +import type { MtprotoInboundSettings } from '@/schemas/protocols/inbound/mtproto'; import type { ShadowsocksClient, ShadowsocksInboundSettings } from '@/schemas/protocols/inbound/shadowsocks'; import type { TrojanClient, TrojanInboundSettings } from '@/schemas/protocols/inbound/trojan'; import type { TunInboundSettings } from '@/schemas/protocols/inbound/tun'; @@ -200,6 +201,43 @@ export function createDefaultMixedInboundSettings(): MixedInboundSettings { }; } +function domainToHex(domain: string): string { + return Array.from(new TextEncoder().encode(domain)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} + +// generateMtprotoSecret builds an "ee" FakeTLS secret: the marker, 16 random +// bytes (32 hex chars), then the domain encoded as hex. Mirrors the Go +// model.GenerateFakeTLSSecret; the backend re-derives it on save so this is +// only for immediate display in the form. +export function generateMtprotoSecret(domain: string): string { + return `ee${RandomUtil.randomSeq(32, { type: 'hex' })}${domainToHex(domain)}`; +} + +// mtprotoSecretForDomain rewrites only the domain suffix of an existing secret, +// preserving its 16-byte random middle when valid (generating one otherwise). +// Mirrors the Go model.HealMtprotoSecret so editing the FakeTLS domain doesn't +// needlessly rotate the secret's identity. +export function mtprotoSecretForDomain(currentSecret: string, domain: string): string { + let body = currentSecret; + if (body.startsWith('ee') || body.startsWith('dd')) { + body = body.slice(2); + } + const middle = /^[0-9a-f]{32}/i.test(body) + ? body.slice(0, 32) + : RandomUtil.randomSeq(32, { type: 'hex' }); + return `ee${middle}${domainToHex(domain)}`; +} + +export function createDefaultMtprotoInboundSettings(): MtprotoInboundSettings { + const fakeTlsDomain = 'www.cloudflare.com'; + return { + fakeTlsDomain, + secret: generateMtprotoSecret(fakeTlsDomain), + }; +} + export function createDefaultTunnelInboundSettings(): TunnelInboundSettings { return { portMap: {}, @@ -261,7 +299,8 @@ export type AnyInboundSettings = | MixedInboundSettings | TunInboundSettings | TunnelInboundSettings - | WireguardInboundSettings; + | WireguardInboundSettings + | MtprotoInboundSettings; export function createDefaultInboundSettings(protocol: string): AnyInboundSettings | null { switch (protocol) { @@ -275,6 +314,7 @@ export function createDefaultInboundSettings(protocol: string): AnyInboundSettin case 'tunnel': return createDefaultTunnelInboundSettings(); case 'tun': return createDefaultTunInboundSettings(); case 'wireguard': return createDefaultWireguardInboundSettings(); + case 'mtproto': return createDefaultMtprotoInboundSettings(); default: return null; } } diff --git a/frontend/src/lib/xray/inbound-link.ts b/frontend/src/lib/xray/inbound-link.ts index 1d8538e9..fdd8e1e5 100644 --- a/frontend/src/lib/xray/inbound-link.ts +++ b/frontend/src/lib/xray/inbound-link.ts @@ -680,6 +680,28 @@ export function genHysteriaLink(input: GenHysteriaLinkInput): string { return url.toString(); } +export interface GenMtprotoLinkInput { + inbound: Inbound; + address: string; + port?: number; + remark?: string; +} + +// Builds a Telegram proxy deep link for an mtproto inbound: +// tg://proxy?server=&port=&secret=. +export function genMtprotoLink(input: GenMtprotoLinkInput): string { + const { inbound, address, port = inbound.port, remark = '' } = input; + if (inbound.protocol !== 'mtproto') return ''; + const secret = inbound.settings.secret ?? ''; + if (secret.length === 0) return ''; + const url = new URL('tg://proxy'); + url.searchParams.set('server', address); + url.searchParams.set('port', String(port)); + url.searchParams.set('secret', secret); + url.hash = encodeURIComponent(remark); + return url.toString(); +} + export interface GenWireguardLinkInput { settings: WireguardInboundSettings; address: string; @@ -867,6 +889,8 @@ export function genLink(input: GenLinkInput): string { clientAuth: client.auth ?? '', externalProxy, }); + case 'mtproto': + return genMtprotoLink({ inbound, address, port, remark }); default: return ''; } diff --git a/frontend/src/pages/inbounds/form/InboundFormModal.tsx b/frontend/src/pages/inbounds/form/InboundFormModal.tsx index 1311e8b9..1b0df6cc 100644 --- a/frontend/src/pages/inbounds/form/InboundFormModal.tsx +++ b/frontend/src/pages/inbounds/form/InboundFormModal.tsx @@ -54,6 +54,7 @@ import { HttpFields, HysteriaFields, MixedFields, + MtprotoFields, ShadowsocksFields, TunFields, TunnelFields, @@ -578,6 +579,8 @@ export default function InboundFormModal({ {protocol === Protocols.HTTP && } {protocol === Protocols.MIXED && } + {protocol === Protocols.MTPROTO && } + {protocol === Protocols.SHADOWSOCKS && } {protocol === Protocols.VLESS && } @@ -883,6 +886,7 @@ export default function InboundFormModal({ Protocols.TUNNEL, Protocols.TUN, Protocols.WIREGUARD, + Protocols.MTPROTO, ] as string[]).includes(protocol) || isFallbackHost ? [{ key: 'protocol', label: t('pages.inbounds.protocol'), children: protocolTab, forceRender: true }] : []), diff --git a/frontend/src/pages/inbounds/form/protocols/index.ts b/frontend/src/pages/inbounds/form/protocols/index.ts index a92744e6..455f66c0 100644 --- a/frontend/src/pages/inbounds/form/protocols/index.ts +++ b/frontend/src/pages/inbounds/form/protocols/index.ts @@ -5,4 +5,5 @@ export { default as WireguardFields } from './wireguard'; export { default as HysteriaFields } from './hysteria'; export { default as HttpFields } from './http'; export { default as MixedFields } from './mixed'; +export { default as MtprotoFields } from './mtproto'; export { default as VlessFields } from './vless'; diff --git a/frontend/src/pages/inbounds/form/protocols/mtproto.tsx b/frontend/src/pages/inbounds/form/protocols/mtproto.tsx new file mode 100644 index 00000000..6826a4ac --- /dev/null +++ b/frontend/src/pages/inbounds/form/protocols/mtproto.tsx @@ -0,0 +1,40 @@ +import { useTranslation } from 'react-i18next'; +import { Alert, Button, Form, Input, Space } from 'antd'; +import { ReloadOutlined } from '@ant-design/icons'; + +import { generateMtprotoSecret, mtprotoSecretForDomain } from '@/lib/xray/inbound-defaults'; + +export default function MtprotoFields() { + const { t } = useTranslation(); + const form = Form.useFormInstance(); + return ( + <> + + { + const current = (form.getFieldValue(['settings', 'secret']) as string) ?? ''; + form.setFieldValue(['settings', 'secret'], mtprotoSecretForDomain(current, e.target.value)); + }} + /> + + + + + + +