mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-08 21:34:33 +00:00
feat(mtproto): add MTProto (FakeTLS) protocol via managed mtg sidecar (#5076)
* 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
This commit is contained in:
62
web/job/mtproto_job.go
Normal file
62
web/job/mtproto_job.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/logger"
|
||||
"github.com/mhsanaei/3x-ui/v3/mtproto"
|
||||
"github.com/mhsanaei/3x-ui/v3/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v3/xray"
|
||||
)
|
||||
|
||||
// MtprotoJob reconciles the running mtg sidecar processes against the enabled
|
||||
// mtproto inbounds in the database, restarts any that crashed, and folds the
|
||||
// per-inbound traffic scraped from each mtg metrics endpoint into the usual
|
||||
// inbound traffic accounting.
|
||||
type MtprotoJob struct {
|
||||
inboundService service.InboundService
|
||||
}
|
||||
|
||||
// NewMtprotoJob creates a new mtproto reconcile/traffic job instance.
|
||||
func NewMtprotoJob() *MtprotoJob {
|
||||
return new(MtprotoJob)
|
||||
}
|
||||
|
||||
// Run reconciles desired mtproto inbounds with running mtg processes and
|
||||
// records traffic deltas.
|
||||
func (j *MtprotoJob) Run() {
|
||||
inbounds, err := j.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
logger.Warning("mtproto job: get inbounds failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
var desired []mtproto.Instance
|
||||
for _, ib := range inbounds {
|
||||
if ib.Protocol != model.MTProto || !ib.Enable || ib.NodeID != nil {
|
||||
continue
|
||||
}
|
||||
if inst, ok := mtproto.InstanceFromInbound(ib); ok {
|
||||
desired = append(desired, inst)
|
||||
}
|
||||
}
|
||||
|
||||
mgr := mtproto.GetManager()
|
||||
mgr.Reconcile(desired)
|
||||
|
||||
deltas := mgr.CollectTraffic()
|
||||
if len(deltas) == 0 {
|
||||
return
|
||||
}
|
||||
traffics := make([]*xray.Traffic, 0, len(deltas))
|
||||
for _, d := range deltas {
|
||||
traffics = append(traffics, &xray.Traffic{
|
||||
IsInbound: true,
|
||||
Tag: d.Tag,
|
||||
Up: d.Up,
|
||||
Down: d.Down,
|
||||
})
|
||||
}
|
||||
if _, _, err := j.inboundService.AddTraffic(traffics, nil); err != nil {
|
||||
logger.Warning("mtproto job: add traffic failed:", err)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/mtproto"
|
||||
"github.com/mhsanaei/3x-ui/v3/xray"
|
||||
)
|
||||
|
||||
@@ -44,6 +45,13 @@ func (l *Local) withAPI(fn func(api *xray.XrayAPI) error) error {
|
||||
}
|
||||
|
||||
func (l *Local) AddInbound(_ context.Context, ib *model.Inbound) error {
|
||||
if ib.Protocol == model.MTProto {
|
||||
inst, ok := mtproto.InstanceFromInbound(ib)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return mtproto.GetManager().Ensure(inst)
|
||||
}
|
||||
body, err := json.MarshalIndent(ib.GenXrayInboundConfig(), "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -54,6 +62,10 @@ func (l *Local) AddInbound(_ context.Context, ib *model.Inbound) error {
|
||||
}
|
||||
|
||||
func (l *Local) DelInbound(_ context.Context, ib *model.Inbound) error {
|
||||
if ib.Protocol == model.MTProto {
|
||||
mtproto.GetManager().Remove(ib.Id)
|
||||
return nil
|
||||
}
|
||||
return l.withAPI(func(api *xray.XrayAPI) error {
|
||||
return api.DelInbound(ib.Tag)
|
||||
})
|
||||
@@ -68,12 +80,18 @@ func (l *Local) UpdateInbound(ctx context.Context, oldIb, newIb *model.Inbound)
|
||||
}
|
||||
|
||||
func (l *Local) AddUser(_ context.Context, ib *model.Inbound, userMap map[string]any) error {
|
||||
if ib.Protocol == model.MTProto {
|
||||
return nil
|
||||
}
|
||||
return l.withAPI(func(api *xray.XrayAPI) error {
|
||||
return api.AddUser(string(ib.Protocol), ib.Tag, userMap)
|
||||
})
|
||||
}
|
||||
|
||||
func (l *Local) RemoveUser(_ context.Context, ib *model.Inbound, email string) error {
|
||||
if ib.Protocol == model.MTProto {
|
||||
return nil
|
||||
}
|
||||
return l.withAPI(func(api *xray.XrayAPI) error {
|
||||
return api.RemoveUser(ib.Tag, email)
|
||||
})
|
||||
|
||||
@@ -101,7 +101,10 @@ func TestAllAPIsPostgresScale(t *testing.T) {
|
||||
run("GetInboundsSlim", func() error { _, err := inboundSvc.GetInboundsSlim(userId); return err })
|
||||
run("GetInboundDetail", func() error { _, err := inboundSvc.GetInboundDetail(ib.Id); return err })
|
||||
run("GetInboundOptions", func() error { _, err := inboundSvc.GetInboundOptions(userId); return err })
|
||||
run("ListPaged", func() error { _, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{Page: 1, PageSize: 25}); return err })
|
||||
run("ListPaged", func() error {
|
||||
_, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{Page: 1, PageSize: 25})
|
||||
return err
|
||||
})
|
||||
run("ListPaged+search", func() error {
|
||||
_, err := svc.ListPaged(inboundSvc, settingSvc, ClientPageParams{Page: 1, PageSize: 25, Search: "user-0012345"})
|
||||
return err
|
||||
|
||||
@@ -621,6 +621,17 @@ func (s *InboundService) normalizeStreamSettings(inbound *model.Inbound) {
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeMtprotoSecret rebuilds an mtproto inbound's FakeTLS secret so it is
|
||||
// always valid and matches the configured domain before the row is persisted.
|
||||
func (s *InboundService) normalizeMtprotoSecret(inbound *model.Inbound) {
|
||||
if inbound.Protocol != model.MTProto {
|
||||
return
|
||||
}
|
||||
if healed, ok := model.HealMtprotoSecret(inbound.Settings); ok {
|
||||
inbound.Settings = healed
|
||||
}
|
||||
}
|
||||
|
||||
// AddInbound creates a new inbound configuration.
|
||||
// It validates port uniqueness, client email uniqueness, and required fields,
|
||||
// then saves the inbound to the database and optionally adds it to the running Xray instance.
|
||||
@@ -628,6 +639,7 @@ func (s *InboundService) normalizeStreamSettings(inbound *model.Inbound) {
|
||||
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
||||
// Normalize streamSettings based on protocol
|
||||
s.normalizeStreamSettings(inbound)
|
||||
s.normalizeMtprotoSecret(inbound)
|
||||
|
||||
conflict, err := s.checkPortConflict(inbound, 0)
|
||||
if err != nil {
|
||||
@@ -943,6 +955,7 @@ func (s *InboundService) SetInboundEnable(id int, enable bool) (bool, error) {
|
||||
func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
||||
// Normalize streamSettings based on protocol
|
||||
s.normalizeStreamSettings(inbound)
|
||||
s.normalizeMtprotoSecret(inbound)
|
||||
|
||||
conflict, err := s.checkPortConflict(inbound, inbound.Id)
|
||||
if err != nil {
|
||||
|
||||
@@ -22,6 +22,8 @@ func inboundTransports(protocol model.Protocol, streamSettings, settings string)
|
||||
switch protocol {
|
||||
case model.Hysteria, model.WireGuard:
|
||||
return transportUDP
|
||||
case model.MTProto:
|
||||
return transportTCP
|
||||
}
|
||||
|
||||
var bits transportBits
|
||||
|
||||
@@ -122,6 +122,9 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
if inbound.NodeID != nil {
|
||||
continue
|
||||
}
|
||||
if inbound.Protocol == model.MTProto {
|
||||
continue
|
||||
}
|
||||
settings := map[string]any{}
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
|
||||
|
||||
@@ -501,6 +501,9 @@
|
||||
"accounts": "الحسابات",
|
||||
"allowTransparent": "السماح بالشفاف",
|
||||
"encryptionMethod": "طريقة التشفير",
|
||||
"fakeTlsDomain": "نطاق FakeTLS (SNI)",
|
||||
"mtprotoSecret": "المفتاح السري",
|
||||
"mtprotoHint": "يتم تقديم MTProto عبر عملية mtg منفصلة وليس Xray. إعدادات النقل والعملاء لا تنطبق هنا — شارك الرابط أدناه مع تيليجرام.",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "الإصدار",
|
||||
"udpIdleTimeout": "UDP idle timeout (ثانية)",
|
||||
|
||||
@@ -502,6 +502,9 @@
|
||||
"accounts": "Accounts",
|
||||
"allowTransparent": "Allow transparent",
|
||||
"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.",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Version",
|
||||
"udpIdleTimeout": "UDP idle timeout (s)",
|
||||
|
||||
@@ -501,6 +501,9 @@
|
||||
"accounts": "Cuentas",
|
||||
"allowTransparent": "Permitir transparente",
|
||||
"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.",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Versión",
|
||||
"udpIdleTimeout": "UDP idle timeout (s)",
|
||||
|
||||
@@ -501,6 +501,9 @@
|
||||
"accounts": "حسابها",
|
||||
"allowTransparent": "اجازه شفاف",
|
||||
"encryptionMethod": "روش رمزنگاری",
|
||||
"fakeTlsDomain": "دامنه FakeTLS (SNI)",
|
||||
"mtprotoSecret": "کلید مخفی",
|
||||
"mtprotoHint": "پروتکل MTProto توسط یک پردازش جداگانه mtg ارائه میشود، نه Xray. تنظیمات انتقال و کلاینتها اینجا کاربرد ندارند — لینک زیر را با تلگرام به اشتراک بگذارید.",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "نسخه",
|
||||
"udpIdleTimeout": "UDP idle timeout (s)",
|
||||
|
||||
@@ -501,6 +501,9 @@
|
||||
"accounts": "Akun",
|
||||
"allowTransparent": "Izinkan transparan",
|
||||
"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.",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Versi",
|
||||
"udpIdleTimeout": "UDP idle timeout (d)",
|
||||
|
||||
@@ -501,6 +501,9 @@
|
||||
"accounts": "アカウント",
|
||||
"allowTransparent": "透過を許可",
|
||||
"encryptionMethod": "暗号化方式",
|
||||
"fakeTlsDomain": "FakeTLS ドメイン (SNI)",
|
||||
"mtprotoSecret": "シークレット",
|
||||
"mtprotoHint": "MTProto は Xray ではなく独立した mtg プロセスで提供されます。ストリーム設定とクライアントはここでは適用されません。下のリンクを Telegram で共有してください。",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "バージョン",
|
||||
"udpIdleTimeout": "UDP idle timeout (秒)",
|
||||
|
||||
@@ -501,6 +501,9 @@
|
||||
"accounts": "Contas",
|
||||
"allowTransparent": "Permitir transparente",
|
||||
"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.",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Versão",
|
||||
"udpIdleTimeout": "UDP idle timeout (s)",
|
||||
|
||||
@@ -501,6 +501,9 @@
|
||||
"accounts": "Аккаунты",
|
||||
"allowTransparent": "Разрешить прозрачный",
|
||||
"encryptionMethod": "Метод шифрования",
|
||||
"fakeTlsDomain": "Домен FakeTLS (SNI)",
|
||||
"mtprotoSecret": "Секрет",
|
||||
"mtprotoHint": "MTProto обслуживается отдельным процессом mtg, а не Xray. Настройки транспорта и клиенты здесь не применяются — поделитесь ссылкой ниже в Telegram.",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Версия",
|
||||
"udpIdleTimeout": "UDP idle timeout (с)",
|
||||
|
||||
@@ -501,6 +501,9 @@
|
||||
"accounts": "Hesaplar",
|
||||
"allowTransparent": "Şeffafa izin ver",
|
||||
"encryptionMethod": "Şifreleme yöntemi",
|
||||
"fakeTlsDomain": "FakeTLS alan adı (SNI)",
|
||||
"mtprotoSecret": "Gizli anahtar",
|
||||
"mtprotoHint": "MTProto, Xray değil ayrı bir mtg işlemi tarafından sunulur. Aktarım ayarları ve istemciler burada geçerli değildir — aşağıdaki bağlantıyı Telegram ile paylaşın.",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Sürüm",
|
||||
"udpIdleTimeout": "UDP idle timeout (s)",
|
||||
|
||||
@@ -501,6 +501,9 @@
|
||||
"accounts": "Акаунти",
|
||||
"allowTransparent": "Дозволити прозорий",
|
||||
"encryptionMethod": "Метод шифрування",
|
||||
"fakeTlsDomain": "Домен FakeTLS (SNI)",
|
||||
"mtprotoSecret": "Секрет",
|
||||
"mtprotoHint": "MTProto обслуговується окремим процесом mtg, а не Xray. Налаштування транспорту та клієнти тут не застосовуються — поділіться посиланням нижче в Telegram.",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Версія",
|
||||
"udpIdleTimeout": "UDP idle timeout (с)",
|
||||
|
||||
@@ -501,6 +501,9 @@
|
||||
"accounts": "Tài khoản",
|
||||
"allowTransparent": "Cho phép trong suốt",
|
||||
"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.",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "Phiên bản",
|
||||
"udpIdleTimeout": "UDP idle timeout (s)",
|
||||
|
||||
@@ -501,6 +501,9 @@
|
||||
"accounts": "账户",
|
||||
"allowTransparent": "允许透明",
|
||||
"encryptionMethod": "加密方法",
|
||||
"fakeTlsDomain": "FakeTLS 域名 (SNI)",
|
||||
"mtprotoSecret": "密钥",
|
||||
"mtprotoHint": "MTProto 由独立的 mtg 进程提供服务,而非 Xray。传输设置和客户端在此不适用——请将下方链接分享到 Telegram。",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "版本",
|
||||
"udpIdleTimeout": "UDP 空闲超时 (s)",
|
||||
|
||||
@@ -501,6 +501,9 @@
|
||||
"accounts": "帳號",
|
||||
"allowTransparent": "允許透明",
|
||||
"encryptionMethod": "加密方法",
|
||||
"fakeTlsDomain": "FakeTLS 網域 (SNI)",
|
||||
"mtprotoSecret": "金鑰",
|
||||
"mtprotoHint": "MTProto 由獨立的 mtg 程序提供服務,而非 Xray。傳輸設定與用戶端在此不適用——請將下方連結分享至 Telegram。",
|
||||
"visionTestseed": "Vision testseed",
|
||||
"version": "版本",
|
||||
"udpIdleTimeout": "UDP 閒置逾時 (s)",
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/config"
|
||||
"github.com/mhsanaei/3x-ui/v3/logger"
|
||||
"github.com/mhsanaei/3x-ui/v3/mtproto"
|
||||
"github.com/mhsanaei/3x-ui/v3/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v3/web/controller"
|
||||
"github.com/mhsanaei/3x-ui/v3/web/job"
|
||||
@@ -281,6 +282,11 @@ func (s *Server) startTask(restartXray bool) {
|
||||
s.cron.AddJob("@every 5s", job.NewXrayTrafficJob())
|
||||
}()
|
||||
|
||||
// Reconcile mtproto (mtg) sidecars and scrape their traffic
|
||||
mtJob := job.NewMtprotoJob()
|
||||
s.cron.AddJob("@every 10s", mtJob)
|
||||
go mtJob.Run()
|
||||
|
||||
// check client ips from log file every 10 sec
|
||||
s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())
|
||||
|
||||
@@ -465,6 +471,7 @@ func (s *Server) stop(stopXray bool, stopTgBot bool) error {
|
||||
s.cancel()
|
||||
if stopXray {
|
||||
s.xrayService.StopXray()
|
||||
mtproto.GetManager().StopAll()
|
||||
}
|
||||
if s.cron != nil {
|
||||
s.cron.Stop()
|
||||
|
||||
Reference in New Issue
Block a user