9 Commits c6f15cd53f ... 21e01cc1e6

Autore SHA1 Messaggio Data
  Rouzbeh† 21e01cc1e6 fix(postgres): make node traffic sync robust after public API inbound updates (#5038) 1 giorno fa
  jq 46684dd164 fix(sub): emit VLESS encryption in Clash configs (#5053) 1 giorno fa
  Sanaei 1ca5924a44 feat(mtproto): add MTProto (FakeTLS) protocol via managed mtg sidecar (#5076) 1 giorno fa
  Sanaei af3c808444 fix: default hysteria tls to no utls fingerprint 1 giorno fa
  shazzreab 98ba88037c fix(subClashService): improve merging of clash rules in YAML (#5054) 1 giorno fa
  Roman Gogolev d739bcf71e fix arm architecture xray binary file name (#5060) 1 giorno fa
  Turan b0fe21c804 i18n(tr): Improve Turkish translation consistency and terminology (#5066) 1 giorno fa
  Turan f6558571b4 docs(i18n): Add Turkish translation for README (#5067) 1 giorno fa
  Tokenicrat 词元 4e253588ae fix(update.sh): allow skipping ssl setup when updating (#5071) 1 giorno fa
58 ha cambiato i file con 2097 aggiunte e 259 eliminazioni
  1. 19 0
      .github/workflows/release.yml
  2. 12 0
      DockerInit.sh
  3. 177 0
      README.tr_TR.md
  4. 25 1
      database/dialect.go
  5. 68 1
      database/model/model.go
  6. 71 0
      database/model/model_mtproto_test.go
  7. 2 1
      frontend/public/openapi.json
  8. 2 1
      frontend/src/generated/schemas.ts
  9. 1 1
      frontend/src/generated/zod.ts
  10. 41 1
      frontend/src/lib/xray/inbound-defaults.ts
  11. 24 0
      frontend/src/lib/xray/inbound-link.ts
  12. 34 0
      frontend/src/lib/xray/inbound-tls-defaults.ts
  13. 19 0
      frontend/src/lib/xray/stream-wire-normalize.ts
  14. 6 13
      frontend/src/pages/inbounds/form/InboundFormModal.tsx
  15. 1 0
      frontend/src/pages/inbounds/form/protocols/index.ts
  16. 40 0
      frontend/src/pages/inbounds/form/protocols/mtproto.tsx
  17. 2 14
      frontend/src/pages/inbounds/form/useSecurityActions.ts
  18. 29 0
      frontend/src/pages/inbounds/info/InboundInfoModal.tsx
  19. 2 0
      frontend/src/schemas/primitives/protocol.ts
  20. 3 0
      frontend/src/schemas/protocols/inbound/index.ts
  21. 10 0
      frontend/src/schemas/protocols/inbound/mtproto.ts
  22. 4 1
      frontend/src/schemas/protocols/security/tls.ts
  23. 168 0
      frontend/src/test/__snapshots__/protocol-capabilities.test.ts.snap
  24. 10 0
      frontend/src/test/__snapshots__/protocols.test.ts.snap
  25. 7 0
      frontend/src/test/golden/fixtures/inbound/mtproto-basic.json
  26. 16 0
      frontend/src/test/inbound-defaults.test.ts
  27. 68 0
      frontend/src/test/stream-wire-normalize.test.ts
  28. 9 0
      install.sh
  29. 348 0
      mtproto/manager.go
  30. 56 0
      mtproto/manager_test.go
  31. 201 0
      mtproto/process.go
  32. 7 0
      mtproto/process_other.go
  33. 66 0
      mtproto/process_windows.go
  34. 12 5
      sub/subClashService.go
  35. 59 0
      sub/subClashService_test.go
  36. 32 3
      sub/subService.go
  37. 39 2
      update.sh
  38. 62 0
      web/job/mtproto_job.go
  39. 18 0
      web/runtime/local.go
  40. 4 1
      web/service/api_scale_postgres_test.go
  41. 55 1
      web/service/inbound.go
  42. 2 0
      web/service/port_conflict.go
  43. 3 0
      web/service/xray.go
  44. 3 0
      web/translation/ar-EG.json
  45. 3 0
      web/translation/en-US.json
  46. 3 0
      web/translation/es-ES.json
  47. 3 0
      web/translation/fa-IR.json
  48. 3 0
      web/translation/id-ID.json
  49. 3 0
      web/translation/ja-JP.json
  50. 3 0
      web/translation/pt-BR.json
  51. 3 0
      web/translation/ru-RU.json
  52. 215 212
      web/translation/tr-TR.json
  53. 3 0
      web/translation/uk-UA.json
  54. 3 0
      web/translation/vi-VN.json
  55. 3 0
      web/translation/zh-CN.json
  56. 3 0
      web/translation/zh-TW.json
  57. 7 0
      web/web.go
  58. 5 1
      xray/process.go

+ 19 - 0
.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 ..

+ 12 - 0
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

+ 177 - 0
README.tr_TR.md

@@ -0,0 +1,177 @@
+[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md) | [Türkçe](/README.tr_TR.md)
+
+<p align="center">
+  <picture>
+    <source media="(prefers-color-scheme: dark)" srcset="./media/3x-ui-dark.png">
+    <img alt="3x-ui" src="./media/3x-ui-light.png">
+  </picture>
+</p>
+
+<p align="center">
+  <a href="https://github.com/MHSanaei/3x-ui/releases"><img src="https://img.shields.io/github/v/release/mhsanaei/3x-ui" alt="Release"></a>
+  <a href="https://github.com/MHSanaei/3x-ui/actions"><img src="https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg" alt="Build"></a>
+  <a href="#"><img src="https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg" alt="GO Version"></a>
+  <a href="https://github.com/MHSanaei/3x-ui/releases/latest"><img src="https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg" alt="Downloads"></a>
+  <a href="https://www.gnu.org/licenses/gpl-3.0.en.html"><img src="https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true" alt="License"></a>
+  <a href="https://pkg.go.dev/github.com/mhsanaei/3x-ui/v3"><img src="https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v3.svg" alt="Go Reference"></a>
+  <a href="https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v3"><img src="https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v3" alt="Go Report Card"></a>
+</p>
+
+**3X-UI**, [Xray-core](https://github.com/XTLS/Xray-core) sunucularını yönetmek için geliştirilmiş, gelişmiş ve açık kaynaklı bir web kontrol panelidir. Tek bir VPS'den çok düğümlü (multi-node) kurulumlara kadar çok çeşitli proxy ve VPN protokollerini kurmak, yapılandırmak ve izlemek için temiz, çok dilli bir arayüz sağlar.
+
+Orijinal X-UI projesinin geliştirilmiş bir çatalı (fork) olarak inşa edilen 3X-UI; daha geniş protokol desteği, iyileştirilmiş kararlılık, kullanıcı başına (per-client) trafik takibi ve birçok yaşam kalitesi (QoL) özelliği ekler.
+
+> [!IMPORTANT]
+> Bu proje yalnızca kişisel kullanım içindir. Lütfen yasa dışı amaçlarla veya üretim (production) ortamında kullanmayın.
+
+## Özellikler
+
+- **Çoklu protokol destekli bağlantı noktaları (Inbounds)** — VLESS, VMess, Trojan, Shadowsocks, WireGuard, Hysteria2, HTTP, SOCKS (Mixed), Dokodemo-door / Tunnel ve TUN.
+- **Modern aktarım (Transport) & güvenlik** — TCP (Raw), mKCP, WebSocket, gRPC, HTTPUpgrade ve XHTTP; TLS, XTLS ve REALITY ile güvenli hale getirilmiştir.
+- **Yedek bağlantılar (Fallbacks)** — Xray'in fallback desteğini kullanarak tek bir port (örn. 443) üzerinden birden fazla protokol (örn. VLESS ve Trojan) sunma.
+- **Kullanıcı bazlı yönetim** — Trafik kotaları, son kullanma tarihleri, IP sınırları, canlı çevrimiçi (online) durumu ve tek tıklamayla paylaşım bağlantıları, QR kodları ve abonelikler.
+- **Trafik istatistikleri** — Bağlantı noktası, kullanıcı ve çıkış noktası (outbound) bazında istatistikler ve sıfırlama kontrolleri.
+- **Çoklu düğüm (Multi-node) desteği** — Tek bir panelden birden fazla sunucuyu yönetin ve ölçeklendirin.
+- **Çıkış noktaları & yönlendirme (Outbound & Routing)** — WARP, NordVPN, özel yönlendirme kuralları, yük dengeleyiciler (load balancers) ve çıkış noktası proxy zincirleme.
+- **Dahili abonelik sunucusu** (Birden fazla çıktı formatıyla).
+- **Telegram botu** (Uzaktan izleme ve yönetim için).
+- **RESTful API** (Panel içi Swagger dokümantasyonu ile).
+- **Esnek veritabanı** — SQLite (varsayılan) veya PostgreSQL.
+- **13 Kullanıcı Arayüzü (UI) dili** (Karanlık ve aydınlık tema destekli).
+- **Fail2ban entegrasyonu** (Kullanıcı bazlı IP sınırlarını zorlamak için).
+
+## Ekran Görüntüleri
+
+<details>
+<summary>Genişletmek için tıklayın</summary>
+
+<picture>
+  <source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
+  <img alt="Genel Bakış" src="./media/01-overview-light.png">
+</picture>
+
+<picture>
+  <source media="(prefers-color-scheme: dark)" srcset="./media/02-add-inbound-dark.png">
+  <img alt="Bağlantı Noktaları" src="./media/02-add-inbound-light.png">
+</picture>
+
+<picture>
+  <source media="(prefers-color-scheme: dark)" srcset="./media/03-add-client-dark.png">
+  <img alt="Kullanıcı Ekle" src="./media/03-add-client-light.png">
+</picture>
+
+<picture>
+  <source media="(prefers-color-scheme: dark)" srcset="./media/05-add-nodes-dark.png">
+  <img alt="Yapılandırmalar" src="./media/05-add-nodes-light.png">
+</picture>
+
+</details>
+
+## Hızlı Başlangıç
+
+```bash
+bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
+```
+
+Kurulum sırasında rastgele bir kullanıcı adı, şifre ve erişim yolu (path) oluşturulur. Kurulumdan sonra `x-ui` komutunu çalıştırarak yönetim menüsünü açabilir; buradan hizmeti başlatabilir/durdurabilir, giriş bilgilerinizi görüntüleyebilir veya sıfırlayabilir, SSL sertifikalarını yönetebilir ve daha fazlasını yapabilirsiniz.
+
+Tam dokümantasyon için lütfen [proje Wiki sayfasını](https://github.com/MHSanaei/3x-ui/wiki) ziyaret edin.
+
+## Desteklenen Platformlar
+
+**İşletim sistemleri:** Ubuntu, Debian, Armbian, Fedora, CentOS, RHEL, AlmaLinux, Rocky Linux, Oracle Linux, Amazon Linux, Virtuozzo, Arch, Manjaro, Parch, openSUSE (Tumbleweed / Leap), Alpine ve Windows.
+
+**Mimariler:** `amd64` · `386` · `arm64` (aarch64) · `armv7` · `armv6` · `armv5` · `s390x`.
+
+## Veritabanı Seçenekleri
+
+3X-UI, kurulum sırasında seçilebilen iki arka uç (backend) destekler:
+
+- **SQLite** (varsayılan) — `/etc/x-ui/x-ui.db` konumunda tek bir dosya. Sıfır kurulum gerektirir, küçük ve orta ölçekli dağıtımlar için idealdir.
+- **PostgreSQL** — Yüksek kullanıcı sayıları veya çok düğümlü (multi-node) kurulumlar için önerilir. Yükleyici sizin için PostgreSQL'i yerel olarak kurabilir veya mevcut bir sunucuya DSN ile bağlanabilir.
+
+Çalışma anında arka uç, ortam değişkenleri (environment variables) aracılığıyla seçilir (yükleyici bunları sizin için `/etc/default/x-ui` dosyasına yazar):
+
+```
+XUI_DB_TYPE=postgres
+XUI_DB_DSN=postgres://xui:[email protected]:5432/xui?sslmode=disable
+```
+
+### Mevcut bir SQLite kurulumunu PostgreSQL'e taşıma
+
+```bash
+x-ui migrate-db --dsn "postgres://xui:[email protected]:5432/xui?sslmode=disable"
+# Ardından /etc/default/x-ui dosyasında XUI_DB_TYPE ve XUI_DB_DSN değerlerini ayarlayıp yeniden başlatın:
+systemctl restart x-ui
+```
+
+Kaynak SQLite dosyasına dokunulmaz; yeni arka ucu doğruladıktan sonra eski dosyayı manuel olarak silebilirsiniz.
+
+### Docker
+
+Varsayılan `docker compose up -d` komutu SQLite kullanmaya devam eder. Dahili PostgreSQL hizmetiyle çalıştırmak için `docker-compose.yml` dosyasındaki iki `XUI_DB_*` ortam değişkeni satırının başındaki yorum işaretini kaldırın ve profille başlatın:
+
+```bash
+docker compose --profile postgres up -d
+```
+
+İmaj, kullanıcı bazlı **IP sınırlarını** zorlamak için Fail2ban'i (varsayılan olarak etkindir) içerir. Fail2ban, ihlalcileri `iptables` ile engeller ve bu işlem `NET_ADMIN` yetkisi gerektirir. `docker-compose.yml` bunu `cap_add` aracılığıyla zaten sağlar; eğer container'ı bunun yerine `docker run` ile başlatırsanız yetkileri kendiniz eklemelisiniz, aksi takdirde engellemeler sadece günlüğe (log) kaydedilir ancak asla uygulanmaz:
+
+```bash
+docker run -d --cap-add=NET_ADMIN --cap-add=NET_RAW ... ghcr.io/mhsanaei/3x-ui
+```
+
+## Ortam Değişkenleri
+
+| Değişken | Açıklama | Varsayılan |
+| --- | --- | --- |
+| `XUI_DB_TYPE` | Veritabanı arka ucu: `sqlite` veya `postgres` | `sqlite` |
+| `XUI_DB_DSN` | PostgreSQL bağlantı dizesi (`XUI_DB_TYPE=postgres` olduğunda) | — |
+| `XUI_DB_FOLDER` | SQLite veritabanı dosyası için dizin | `/etc/x-ui` |
+| `XUI_DB_MAX_OPEN_CONNS` | Maksimum açık bağlantı sayısı (PostgreSQL havuzu) | — |
+| `XUI_DB_MAX_IDLE_CONNS` | Maksimum boşta bağlantı sayısı (PostgreSQL havuzu) | — |
+| `XUI_ENABLE_FAIL2BAN` | Fail2ban tabanlı IP sınırı zorlamasını etkinleştir | `true` |
+| `XUI_LOG_LEVEL` | Log detay seviyesi (`debug`, `info`, `warning`, `error`) | `info` |
+| `XUI_DEBUG` | Hata ayıklama (debug) modunu etkinleştir | `false` |
+
+## Desteklenen Diller
+
+Panel kullanıcı arayüzü 13 dilde mevcuttur:
+
+English · فارسی · العربية · 中文(简体) · 中文(繁體) · Español · Русский · Українська · Türkçe · Tiếng Việt · 日本語 · Bahasa Indonesia · Português (Brasil)
+
+## Katkıda Bulunma
+
+Katkılara açığız. Lütfen bir sorun (issue) veya çekme isteği (pull request) açmadan önce [Katkıda Bulunma Rehberi](/CONTRIBUTING.md)'ni okuyun.
+
+## Özel Teşekkürler
+
+- [alireza0](https://github.com/alireza0/)
+
+## Teşekkür
+
+- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Lisans: **GPL-3.0**): _Dahili İran alan adları ve güvenlik/reklam engelleme odaklı geliştirilmiş v2ray/xray ve v2ray/xray-clients yönlendirme kuralları._
+- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (Lisans: **GPL-3.0**): _Rusya'daki engellenmiş alan adları ve adreslere dayalı otomatik olarak güncellenen V2Ray yönlendirme kuralları içerir._
+
+## Topluluk Araçları
+
+3x-ui etrafında topluluk tarafından geliştirilen araçlar ve entegrasyonlar.
+
+- [terraform-provider-3x-ui](https://github.com/batonogov/terraform-provider-threexui) (Lisans: **MIT**): _Bağlantı noktalarını, kullanıcıları, panel ayarlarını ve Xray yapılandırmasını Terraform / OpenTofu ile kod olarak yönetin._
+
+## Projeyi Destekleyin
+
+**Eğer bu proje sizin için faydalıysa, bir :star2: (yıldız) verebilirsiniz.**
+
+<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
+<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
+</a>
+
+</br>
+<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
+   <img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
+</a>
+
+## Zaman İçindeki Yıldız Sayısı
+
+[![Zaman İçindeki Yıldız Sayısı](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)

+ 25 - 1
database/dialect.go

@@ -22,7 +22,31 @@ func JSONFieldText(expr, key string) string {
 
 func GreatestExpr(a, b string) string {
 	if IsPostgres() {
-		return fmt.Sprintf("GREATEST(%s, %s)", a, b)
+		return fmt.Sprintf("GREATEST(%s::bigint, %s::bigint)", a, b)
 	}
 	return fmt.Sprintf("MAX(%s, %s)", a, b)
 }
+
+// ClientTrafficEnableMergeExpr returns the SQL expression used in the
+// node traffic merge to update client_traffics.enable.
+//
+// The intent is: only allow the remote node to *disable* a client
+// (never re-enable one that the central panel has disabled).
+//
+// We use a dialect-specific expression because:
+// - On PostgreSQL we want strict boolean typing and casts to avoid
+//   "CASE types boolean and integer cannot be matched" errors
+//   (and similar internal expansions of AND/GREATEST).
+// - On SQLite, enable is stored with INTEGER affinity (0/1), there is
+//   no :: cast syntax, and we must produce a numeric-compatible result.
+//
+// The expression must be valid SQL for tx.Exec with a boolean parameter
+// as the first ?.
+func ClientTrafficEnableMergeExpr() string {
+	if IsPostgres() {
+		return "CASE WHEN ?::boolean THEN enable::boolean ELSE false END"
+	}
+	// SQLite: no :: casts. Use numeric CASE. 1/0 work as true/false
+	// thanks to SQLite's affinity and how GORM/drivers bind bools.
+	return "CASE WHEN ? THEN enable ELSE 0 END"
+}

+ 68 - 1
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"`

+ 71 - 0
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")
+	}
+}

+ 2 - 1
frontend/public/openapi.json

@@ -1341,7 +1341,8 @@
               "http",
               "mixed",
               "tunnel",
-              "tun"
+              "tun",
+              "mtproto"
             ],
             "example": "vless",
             "type": "string"

+ 2 - 1
frontend/src/generated/schemas.ts

@@ -1315,7 +1315,8 @@ export const SCHEMAS: Record<string, unknown> = {
           "http",
           "mixed",
           "tunnel",
-          "tun"
+          "tun",
+          "mtproto"
         ],
         "example": "vless",
         "type": "string"

+ 1 - 1
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(),

+ 41 - 1
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;
   }
 }

+ 24 - 0
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=<addr>&port=<port>&secret=<ee FakeTLS 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 '';
   }

+ 34 - 0
frontend/src/lib/xray/inbound-tls-defaults.ts

@@ -0,0 +1,34 @@
+import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
+
+function defaultCertificate(): Record<string, unknown> {
+  return {
+    useFile: true,
+    certificateFile: '',
+    keyFile: '',
+    certificate: [],
+    key: [],
+    ocspStapling: 3600,
+    oneTimeLoading: false,
+    usage: 'encipherment',
+    buildChain: false,
+  };
+}
+
+export function createTlsSettingsWithDefaultCert(): Record<string, unknown> {
+  const tls = TlsStreamSettingsSchema.parse({}) as Record<string, unknown>;
+  tls.certificates = [defaultCertificate()];
+  return tls;
+}
+
+export function createHysteriaTlsSettingsWithDefaultCert(): Record<string, unknown> {
+  const tls = createTlsSettingsWithDefaultCert();
+  tls.alpn = ['h3'];
+
+  const settings = tls.settings && typeof tls.settings === 'object' && !Array.isArray(tls.settings)
+    ? { ...(tls.settings as Record<string, unknown>) }
+    : {};
+  settings.fingerprint = '';
+  tls.settings = settings;
+
+  return tls;
+}

+ 19 - 0
frontend/src/lib/xray/stream-wire-normalize.ts

@@ -95,6 +95,20 @@ function dropZeroNumbers(obj: Record<string, unknown>, keys: readonly string[]):
   }
 }
 
+function normalizeTlsForWire(raw: Record<string, unknown>): Record<string, unknown> {
+  const out: Record<string, unknown> = { ...raw };
+  if (out.fingerprint === '') delete out.fingerprint;
+
+  const settings = out.settings;
+  if (isRecord(settings)) {
+    const settingsOut: Record<string, unknown> = { ...settings };
+    if (settingsOut.fingerprint === '') delete settingsOut.fingerprint;
+    out.settings = settingsOut;
+  }
+
+  return out;
+}
+
 export function normalizeXhttpForWire(
   raw: Record<string, unknown>,
   side: StreamWireSide,
@@ -211,6 +225,11 @@ export function normalizeStreamSettingsForWire(
     out.xhttpSettings = normalizeXhttpForWire(xhttp, opts.side);
   }
 
+  const tls = out.tlsSettings;
+  if (isRecord(tls)) {
+    out.tlsSettings = normalizeTlsForWire(tls);
+  }
+
   const sockopt = out.sockopt;
   if (isRecord(sockopt)) {
     const normalized = normalizeSockoptForWire(sockopt);

+ 6 - 13
frontend/src/pages/inbounds/form/InboundFormModal.tsx

@@ -36,7 +36,7 @@ import { antdRule } from '@/utils/zodForm';
 import { Protocols } from '@/schemas/primitives';
 import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
 import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria';
-import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
+import { createHysteriaTlsSettingsWithDefaultCert } from '@/lib/xray/inbound-tls-defaults';
 import { SniffingSchema } from '@/schemas/primitives/sniffing';
 import { TcpStreamSettingsSchema } from '@/schemas/protocols/stream/tcp';
 import { KcpStreamSettingsSchema } from '@/schemas/protocols/stream/kcp';
@@ -54,6 +54,7 @@ import {
   HttpFields,
   HysteriaFields,
   MixedFields,
+  MtprotoFields,
   ShadowsocksFields,
   TunFields,
   TunnelFields,
@@ -351,22 +352,11 @@ export default function InboundFormModal({
       // snap back to TCP so the standard network selector has a valid
       // starting point.
       if (next === Protocols.HYSTERIA) {
-        const tls = TlsStreamSettingsSchema.parse({}) as Record<string, unknown>;
-        tls.certificates = [{
-          useFile: true,
-          certificateFile: '',
-          keyFile: '',
-          certificate: [],
-          key: [],
-          oneTimeLoading: false,
-          usage: 'encipherment',
-          buildChain: false,
-        }];
         form.setFieldValue('streamSettings', {
           network: 'hysteria',
           security: 'tls',
           hysteriaSettings: HysteriaStreamSettingsSchema.parse({}),
-          tlsSettings: tls,
+          tlsSettings: createHysteriaTlsSettingsWithDefaultCert(),
           // Hysteria2 needs an obfs wrapper on the FinalMask side; seed
           // it with salamander + a random password so the listener boots
           // with a usable default. Re-selecting Hysteria from another
@@ -589,6 +579,8 @@ export default function InboundFormModal({
       {protocol === Protocols.HTTP && <HttpFields />}
       {protocol === Protocols.MIXED && <MixedFields mixedUdpOn={mixedUdpOn} />}
 
+      {protocol === Protocols.MTPROTO && <MtprotoFields />}
+
       {protocol === Protocols.SHADOWSOCKS && <ShadowsocksFields form={form} isSSWith2022={isSSWith2022} />}
 
       {protocol === Protocols.VLESS && <VlessFields saving={saving} selectedVlessAuth={selectedVlessAuth} network={network} security={security} getNewVlessEnc={getNewVlessEnc} clearVlessEnc={clearVlessEnc} />}
@@ -894,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 }]
               : []),

+ 1 - 0
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';

+ 40 - 0
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 (
+    <>
+      <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 wrapperCol={{ span: 24 }}>
+        <Alert type="info" showIcon message={t('pages.inbounds.form.mtprotoHint')} />
+      </Form.Item>
+    </>
+  );
+}

+ 2 - 14
frontend/src/pages/inbounds/form/useSecurityActions.ts

@@ -5,7 +5,7 @@ import type { MessageInstance } from 'antd/es/message/interface';
 
 import { HttpUtil, RandomUtil } from '@/utils';
 import { getRandomRealityTarget } from '@/models/reality-targets';
-import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
+import { createTlsSettingsWithDefaultCert } from '@/lib/xray/inbound-tls-defaults';
 import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
 import type { InboundFormValues } from '@/schemas/forms/inbound-form';
 
@@ -160,19 +160,7 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseS
     delete cleaned.tlsSettings;
     delete cleaned.realitySettings;
     if (next === 'tls') {
-      const tls = TlsStreamSettingsSchema.parse({}) as Record<string, unknown>;
-      tls.certificates = [{
-        useFile: true,
-        certificateFile: '',
-        keyFile: '',
-        certificate: [],
-        key: [],
-        ocspStapling: 3600,
-        oneTimeLoading: false,
-        usage: 'encipherment',
-        buildChain: false,
-      }];
-      cleaned.tlsSettings = tls;
+      cleaned.tlsSettings = createTlsSettingsWithDefaultCert();
     }
     if (next === 'reality') {
       const reality = RealityStreamSettingsSchema.parse({}) as Record<string, unknown>;

+ 29 - 0
frontend/src/pages/inbounds/info/InboundInfoModal.tsx

@@ -625,6 +625,35 @@ export default function InboundInfoModal({
         </dl>
       )}
 
+      {inbound.protocol === Protocols.MTPROTO && inbound.settings && (
+        <dl className="info-list info-list-block">
+          <div className="info-row">
+            <dt>{t('pages.inbounds.form.fakeTlsDomain')}</dt>
+            <dd><Tag color="green" className="value-tag">{inbound.settings.fakeTlsDomain as string}</Tag></dd>
+          </div>
+          <div className="info-row">
+            <dt>{t('pages.inbounds.form.mtprotoSecret')}</dt>
+            <dd className="value-block">
+              <code className="value-code">{inbound.settings.secret as string}</code>
+              <Tooltip title={t('copy')}>
+                <Button size="small" className="value-copy" icon={<CopyOutlined />} onClick={() => copyText(inbound.settings.secret as string, t)} />
+              </Tooltip>
+            </dd>
+          </div>
+          {links.length > 0 && (
+            <div className="info-row">
+              <dt>{t('pages.inbounds.copyLink')}</dt>
+              <dd className="value-block">
+                <code className="value-code">{links[0].link}</code>
+                <Tooltip title={t('copy')}>
+                  <Button size="small" className="value-copy" icon={<CopyOutlined />} onClick={() => copyText(links[0].link, t)} />
+                </Tooltip>
+              </dd>
+            </div>
+          )}
+        </dl>
+      )}
+
       {dbInbound.isMixed && inbound.settings && (
         <dl className="info-list info-list-block">
           <div className="info-row">

+ 2 - 0
frontend/src/schemas/primitives/protocol.ts

@@ -11,6 +11,7 @@ export const ProtocolSchema = z.enum([
   'mixed',
   'tunnel',
   'tun',
+  'mtproto',
 ]);
 export type Protocol = z.infer<typeof ProtocolSchema>;
 
@@ -31,4 +32,5 @@ export const Protocols = Object.freeze({
   MIXED: 'mixed',
   TUNNEL: 'tunnel',
   TUN: 'tun',
+  MTPROTO: 'mtproto',
 });

+ 3 - 0
frontend/src/schemas/protocols/inbound/index.ts

@@ -3,6 +3,7 @@ import { z } from 'zod';
 import { HttpInboundSettingsSchema } from './http';
 import { HysteriaInboundSettingsSchema } from './hysteria';
 import { MixedInboundSettingsSchema } from './mixed';
+import { MtprotoInboundSettingsSchema } from './mtproto';
 import { ShadowsocksInboundSettingsSchema } from './shadowsocks';
 import { TrojanInboundSettingsSchema } from './trojan';
 import { TunInboundSettingsSchema } from './tun';
@@ -14,6 +15,7 @@ import { WireguardInboundSettingsSchema } from './wireguard';
 export * from './http';
 export * from './hysteria';
 export * from './mixed';
+export * from './mtproto';
 export * from './shadowsocks';
 export * from './trojan';
 export * from './tun';
@@ -38,5 +40,6 @@ export const InboundSettingsSchema = z.discriminatedUnion('protocol', [
   z.object({ protocol: z.literal('mixed'),       settings: MixedInboundSettingsSchema }),
   z.object({ protocol: z.literal('tunnel'),      settings: TunnelInboundSettingsSchema }),
   z.object({ protocol: z.literal('tun'),         settings: TunInboundSettingsSchema }),
+  z.object({ protocol: z.literal('mtproto'),     settings: MtprotoInboundSettingsSchema }),
 ]);
 export type InboundSettings = z.infer<typeof InboundSettingsSchema>;

+ 10 - 0
frontend/src/schemas/protocols/inbound/mtproto.ts

@@ -0,0 +1,10 @@
+import { z } from 'zod';
+
+// 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.
+export const MtprotoInboundSettingsSchema = z.object({
+  fakeTlsDomain: z.string().default('www.cloudflare.com'),
+  secret: z.string().default(''),
+});
+export type MtprotoInboundSettings = z.infer<typeof MtprotoInboundSettingsSchema>;

+ 4 - 1
frontend/src/schemas/protocols/security/tls.ts

@@ -22,6 +22,9 @@ export const UtlsFingerprintSchema = z.enum([
 ]);
 export type UtlsFingerprint = z.infer<typeof UtlsFingerprintSchema>;
 
+export const TlsFingerprintSchema = z.union([UtlsFingerprintSchema, z.literal('')]);
+export type TlsFingerprint = z.infer<typeof TlsFingerprintSchema>;
+
 export const AlpnSchema = z.enum(['h3', 'h2', 'http/1.1']);
 export type Alpn = z.infer<typeof AlpnSchema>;
 
@@ -51,7 +54,7 @@ export const TlsCertSchema = z.union([TlsCertFileSchema, TlsCertInlineSchema]);
 export type TlsCert = z.infer<typeof TlsCertSchema>;
 
 export const TlsClientSettingsSchema = z.object({
-  fingerprint: UtlsFingerprintSchema.default('chrome'),
+  fingerprint: TlsFingerprintSchema.default('chrome'),
   echConfigList: z.string().default(''),
   pinnedPeerCertSha256: z.array(z.string()).default([]),
 });

+ 168 - 0
frontend/src/test/__snapshots__/protocol-capabilities.test.ts.snap

@@ -504,6 +504,174 @@ exports[`protocol capability predicates > mixed-basic :: xhttp/tls 1`] = `
 }
 `;
 
+exports[`protocol capability predicates > mtproto-basic :: grpc/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mtproto-basic :: grpc/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mtproto-basic :: grpc/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mtproto-basic :: httpupgrade/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mtproto-basic :: httpupgrade/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mtproto-basic :: kcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mtproto-basic :: tcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mtproto-basic :: tcp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mtproto-basic :: tcp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mtproto-basic :: ws/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mtproto-basic :: ws/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mtproto-basic :: xhttp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mtproto-basic :: xhttp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mtproto-basic :: 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,

+ 10 - 0
frontend/src/test/__snapshots__/protocols.test.ts.snap

@@ -59,6 +59,16 @@ exports[`InboundSettingsSchema fixtures > parses mixed-basic byte-stably 1`] = `
 }
 `;
 
+exports[`InboundSettingsSchema fixtures > parses mtproto-basic byte-stably 1`] = `
+{
+  "protocol": "mtproto",
+  "settings": {
+    "fakeTlsDomain": "www.cloudflare.com",
+    "secret": "ee0123456789abcdef0123456789abcdef7777772e636c6f7564666c6172652e636f6d",
+  },
+}
+`;
+
 exports[`InboundSettingsSchema fixtures > parses shadowsocks-2022 byte-stably 1`] = `
 {
   "protocol": "shadowsocks",

+ 7 - 0
frontend/src/test/golden/fixtures/inbound/mtproto-basic.json

@@ -0,0 +1,7 @@
+{
+  "protocol": "mtproto",
+  "settings": {
+    "fakeTlsDomain": "www.cloudflare.com",
+    "secret": "ee0123456789abcdef0123456789abcdef7777772e636c6f7564666c6172652e636f6d"
+  }
+}

+ 16 - 0
frontend/src/test/inbound-defaults.test.ts

@@ -16,6 +16,7 @@ import {
   createDefaultVmessInboundSettings,
   createDefaultWireguardInboundSettings,
 } from '@/lib/xray/inbound-defaults';
+import { createHysteriaTlsSettingsWithDefaultCert } from '@/lib/xray/inbound-tls-defaults';
 import { HttpInboundSettingsSchema } from '@/schemas/protocols/inbound/http';
 import { HysteriaClientSchema, HysteriaInboundSettingsSchema } from '@/schemas/protocols/inbound/hysteria';
 import { MixedInboundSettingsSchema } from '@/schemas/protocols/inbound/mixed';
@@ -147,3 +148,18 @@ describe('createDefault*InboundSettings factories', () => {
     expect(WireguardInboundSettingsSchema.parse(s)).toEqual(s);
   });
 });
+
+describe('createHysteriaTlsSettingsWithDefaultCert', () => {
+  it('defaults Hysteria TLS to uTLS None and h3 ALPN', () => {
+    const tls = createHysteriaTlsSettingsWithDefaultCert();
+    expect(tls.alpn).toEqual(['h3']);
+    expect((tls.settings as Record<string, unknown>).fingerprint).toBe('');
+    expect(tls.certificates).toEqual([
+      expect.objectContaining({
+        useFile: true,
+        certificateFile: '',
+        keyFile: '',
+      }),
+    ]);
+  });
+});

+ 68 - 0
frontend/src/test/stream-wire-normalize.test.ts

@@ -9,6 +9,7 @@ import {
   normalizeXhttpForWire,
   validateRealityTarget,
 } from '@/lib/xray/stream-wire-normalize';
+import { InboundFormSchema } from '@/schemas/forms/inbound-form';
 import type { InboundFormValues } from '@/schemas/forms/inbound-form';
 
 describe('validateRealityTarget', () => {
@@ -150,6 +151,28 @@ describe('normalizeStreamSettingsForWire reality', () => {
   });
 });
 
+describe('normalizeStreamSettingsForWire tls', () => {
+  it('drops empty uTLS fingerprints from inbound and outbound TLS shapes', () => {
+    const out = normalizeStreamSettingsForWire({
+      network: 'hysteria',
+      security: 'tls',
+      tlsSettings: {
+        fingerprint: '',
+        settings: {
+          fingerprint: '',
+          echConfigList: '',
+        },
+      },
+    }, { side: 'inbound' });
+
+    const tls = out.tlsSettings as Record<string, unknown>;
+    const settings = tls.settings as Record<string, unknown>;
+    expect(tls).not.toHaveProperty('fingerprint');
+    expect(settings).not.toHaveProperty('fingerprint');
+    expect(settings.echConfigList).toBe('');
+  });
+});
+
 describe('inbound formValuesToWirePayload integration', () => {
   it('emits lean stream-one xhttp + sockopt on save', () => {
     const values = {
@@ -209,6 +232,51 @@ describe('inbound formValuesToWirePayload integration', () => {
     const realitySettings = reality.settings as Record<string, unknown>;
     expect(realitySettings.publicKey).toBe('pub');
   });
+
+  it('accepts Hysteria TLS with uTLS None and omits fingerprint on save', () => {
+    const values = {
+      remark: 'hy2',
+      enable: true,
+      port: 443,
+      listen: '',
+      tag: 'hy2-443',
+      expiryTime: 0,
+      sniffing: { enabled: false },
+      up: 0,
+      down: 0,
+      total: 0,
+      trafficReset: 'never',
+      lastTrafficResetTime: 0,
+      nodeId: null,
+      protocol: 'hysteria',
+      settings: { version: 2, clients: [] },
+      streamSettings: {
+        network: 'hysteria',
+        security: 'tls',
+        hysteriaSettings: {
+          version: 2,
+          auth: 'auth',
+          udpIdleTimeout: 60,
+        },
+        tlsSettings: {
+          alpn: ['h3'],
+          settings: {
+            fingerprint: '',
+          },
+        },
+      },
+    };
+
+    const parsed = InboundFormSchema.safeParse(values);
+    expect(parsed.success).toBe(true);
+    if (!parsed.success) throw parsed.error;
+
+    const payload = formValuesToWirePayload(parsed.data);
+    const stream = JSON.parse(payload.streamSettings) as Record<string, unknown>;
+    const tls = stream.tlsSettings as Record<string, unknown>;
+    const settings = tls.settings as Record<string, unknown>;
+    expect(settings).not.toHaveProperty('fingerprint');
+  });
 });
 
 describe('freedom outbound sockopt wire payload', () => {

+ 9 - 0
install.sh

@@ -1192,8 +1192,17 @@ install_x-ui() {
     if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
         mv bin/xray-linux-$(arch) bin/xray-linux-arm
         chmod +x bin/xray-linux-arm
+        if [[ -f bin/mtg-linux-$(arch) ]]; then
+            mv bin/mtg-linux-$(arch) bin/mtg-linux-arm
+            chmod +x bin/mtg-linux-arm
+        fi
     fi
     chmod +x x-ui bin/xray-linux-$(arch)
+    if [[ -f bin/mtg-linux-arm ]]; then
+        chmod +x bin/mtg-linux-arm
+    elif [[ -f bin/mtg-linux-$(arch) ]]; then
+        chmod +x bin/mtg-linux-$(arch)
+    fi
 
     # Update x-ui cli and se set permission
     mv -f /usr/bin/x-ui-temp /usr/bin/x-ui

+ 348 - 0
mtproto/manager.go

@@ -0,0 +1,348 @@
+package mtproto
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"net"
+	"net/http"
+	"os"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/mhsanaei/3x-ui/v3/database/model"
+	"github.com/mhsanaei/3x-ui/v3/logger"
+)
+
+// Instance is the desired runtime configuration of one mtproto inbound.
+type Instance struct {
+	Id     int
+	Tag    string
+	Listen string
+	Port   int
+	Secret string
+}
+
+func (inst Instance) bindTo() string {
+	listen := inst.Listen
+	if listen == "" {
+		listen = "0.0.0.0"
+	}
+	return fmt.Sprintf("%s:%d", listen, inst.Port)
+}
+
+func (inst Instance) fingerprint() string {
+	return fmt.Sprintf("%s|%s", inst.bindTo(), inst.Secret)
+}
+
+// Traffic is a per-inbound traffic delta scraped from an mtg metrics endpoint.
+type Traffic struct {
+	Tag  string
+	Up   int64
+	Down int64
+}
+
+type managed struct {
+	proc        *Process
+	tag         string
+	fingerprint string
+	metricsPort int
+	lastUp      int64
+	lastDown    int64
+	haveLast    bool
+}
+
+// Manager owns the set of running mtg processes keyed by inbound id.
+type Manager struct {
+	mu    sync.Mutex
+	procs map[int]*managed
+}
+
+var (
+	managerOnce sync.Once
+	manager     *Manager
+)
+
+// GetManager returns the process-wide mtg manager singleton.
+func GetManager() *Manager {
+	managerOnce.Do(func() {
+		manager = &Manager{procs: map[int]*managed{}}
+	})
+	return manager
+}
+
+// InstanceFromInbound derives a desired Instance from an mtproto inbound,
+// healing the FakeTLS secret so it always matches the configured domain.
+// Returns false when the inbound is not a usable mtproto inbound.
+func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
+	if ib == nil || ib.Protocol != model.MTProto {
+		return Instance{}, false
+	}
+	settings := ib.Settings
+	if healed, ok := model.HealMtprotoSecret(settings); ok {
+		settings = healed
+	}
+	var parsed struct {
+		Secret string `json:"secret"`
+	}
+	if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
+		return Instance{}, false
+	}
+	if parsed.Secret == "" {
+		return Instance{}, false
+	}
+	return Instance{
+		Id:     ib.Id,
+		Tag:    ib.Tag,
+		Listen: ib.Listen,
+		Port:   ib.Port,
+		Secret: parsed.Secret,
+	}, true
+}
+
+// Ensure starts the mtg process for an instance, or restarts it when its
+// configuration changed. A no-op when the desired process is already running.
+func (m *Manager) Ensure(inst Instance) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	return m.ensureLocked(inst)
+}
+
+func (m *Manager) ensureLocked(inst Instance) error {
+	fp := inst.fingerprint()
+	if cur, ok := m.procs[inst.Id]; ok {
+		if cur.fingerprint == fp && cur.proc.IsRunning() {
+			cur.tag = inst.Tag
+			return nil
+		}
+		cur.proc.Stop()
+		delete(m.procs, inst.Id)
+	}
+	metricsPort, err := freeLocalPort()
+	if err != nil {
+		return err
+	}
+	cfgPath := configPathForID(inst.Id)
+	if err := writeConfig(cfgPath, inst.Secret, inst.bindTo(), metricsPort); err != nil {
+		return err
+	}
+	proc := newProcess(cfgPath)
+	if err := proc.Start(); err != nil {
+		return err
+	}
+	m.procs[inst.Id] = &managed{
+		proc:        proc,
+		tag:         inst.Tag,
+		fingerprint: fp,
+		metricsPort: metricsPort,
+	}
+	logger.Info("mtproto: started mtg for inbound", inst.Id, "on", inst.bindTo())
+	return nil
+}
+
+// Remove stops and forgets the mtg process for an inbound id.
+func (m *Manager) Remove(id int) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	if cur, ok := m.procs[id]; ok {
+		cur.proc.Stop()
+		delete(m.procs, id)
+		_ = os.Remove(configPathForID(id))
+		logger.Info("mtproto: stopped mtg for inbound", id)
+	}
+}
+
+// Reconcile drives the running set toward the desired instances: it stops
+// processes that are no longer wanted and (re)starts the rest. Used at boot
+// and periodically to recover from crashes.
+func (m *Manager) Reconcile(desired []Instance) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	want := make(map[int]struct{}, len(desired))
+	for _, inst := range desired {
+		want[inst.Id] = struct{}{}
+	}
+	for id, cur := range m.procs {
+		if _, ok := want[id]; !ok {
+			cur.proc.Stop()
+			delete(m.procs, id)
+			_ = os.Remove(configPathForID(id))
+		}
+	}
+	for _, inst := range desired {
+		if err := m.ensureLocked(inst); err != nil {
+			logger.Warning("mtproto: reconcile failed for inbound", inst.Id, ":", err)
+		}
+	}
+}
+
+// StopAll stops every managed mtg process. Called on panel shutdown.
+func (m *Manager) StopAll() {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	for id, cur := range m.procs {
+		_ = cur.proc.Stop()
+		_ = os.Remove(configPathForID(id))
+		delete(m.procs, id)
+	}
+}
+
+// CollectTraffic scrapes each running mtg metrics endpoint and returns the
+// per-inbound byte deltas since the previous scrape.
+func (m *Manager) CollectTraffic() []Traffic {
+	// Snapshot the state we need under the lock, then release before doing
+	// network I/O so that Ensure/Reconcile/Remove are not blocked.
+	type snap struct {
+		id          int
+		metricsPort int
+		tag         string
+		haveLast    bool
+		lastUp      int64
+		lastDown    int64
+	}
+	m.mu.Lock()
+	snaps := make([]snap, 0, len(m.procs))
+	for id, cur := range m.procs {
+		if cur.proc == nil || !cur.proc.IsRunning() {
+			continue
+		}
+		snaps = append(snaps, snap{
+			id:          id,
+			metricsPort: cur.metricsPort,
+			tag:         cur.tag,
+			haveLast:    cur.haveLast,
+			lastUp:      cur.lastUp,
+			lastDown:    cur.lastDown,
+		})
+	}
+	m.mu.Unlock()
+
+	out := make([]Traffic, 0, len(snaps))
+	for _, s := range snaps {
+		up, down, ok := scrapeTraffic(s.metricsPort)
+		if !ok {
+			continue
+		}
+		var du, dd int64
+		if s.haveLast {
+			du = up - s.lastUp
+			dd = down - s.lastDown
+			if du < 0 {
+				du = 0
+			}
+			if dd < 0 {
+				dd = 0
+			}
+		}
+
+		// Re-acquire lock to persist the new baseline, but only if the entry
+		// still exists (it may have been removed during the scrape).
+		m.mu.Lock()
+		if cur, ok := m.procs[s.id]; ok {
+			cur.lastUp = up
+			cur.lastDown = down
+			cur.haveLast = true
+		}
+		m.mu.Unlock()
+
+		if s.haveLast && (du > 0 || dd > 0) {
+			out = append(out, Traffic{Tag: s.tag, Up: du, Down: dd})
+		}
+	}
+	return out
+}
+
+func freeLocalPort() (int, error) {
+	l, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		return 0, err
+	}
+	defer l.Close()
+	return l.Addr().(*net.TCPAddr).Port, nil
+}
+
+func writeConfig(path, secret, bindTo string, 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)
+}
+
+// scrapeTraffic reads the mtg Prometheus metrics endpoint and sums byte
+// counters by direction. mtg exposes a traffic counter labelled with a
+// direction; "to_telegram" is treated as upload and "to_client" as download.
+// Best-effort: an unreachable endpoint or unrecognised format yields ok=false.
+func scrapeTraffic(port int) (up int64, down int64, ok bool) {
+	client := http.Client{Timeout: 3 * time.Second}
+	resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/metrics", port))
+	if err != nil {
+		return 0, 0, false
+	}
+	defer resp.Body.Close()
+	scanner := bufio.NewScanner(resp.Body)
+	scanner.Buffer(make([]byte, 64*1024), 1024*1024)
+	found := false
+	for scanner.Scan() {
+		line := strings.TrimSpace(scanner.Text())
+		if line == "" || line[0] == '#' || !strings.Contains(line, "traffic") {
+			continue
+		}
+		name, labels, value, perr := parseMetricLine(line)
+		if perr != nil || !strings.HasPrefix(name, "mtg") {
+			continue
+		}
+		switch labels["direction"] {
+		case "to_telegram", "egress", "up":
+			up += int64(value)
+		case "to_client", "ingress", "down":
+			down += int64(value)
+		default:
+			down += int64(value)
+		}
+		found = true
+	}
+	if err := scanner.Err(); err != nil {
+		logger.Debug("mtproto: metrics scan error:", err)
+	}
+	return up, down, found
+}
+
+func parseMetricLine(line string) (name string, labels map[string]string, value float64, err error) {
+	labels = map[string]string{}
+	rest := line
+	if brace := strings.IndexByte(line, '{'); brace >= 0 {
+		name = line[:brace]
+		end := strings.IndexByte(line, '}')
+		if end < brace {
+			return "", nil, 0, fmt.Errorf("malformed metric line")
+		}
+		for _, kv := range strings.Split(line[brace+1:end], ",") {
+			eq := strings.IndexByte(kv, '=')
+			if eq < 0 {
+				continue
+			}
+			labels[strings.TrimSpace(kv[:eq])] = strings.Trim(strings.TrimSpace(kv[eq+1:]), `"`)
+		}
+		rest = strings.TrimSpace(line[end+1:])
+	} else {
+		fields := strings.Fields(line)
+		if len(fields) < 2 {
+			return "", nil, 0, fmt.Errorf("malformed metric line")
+		}
+		name = fields[0]
+		rest = fields[1]
+	}
+	valFields := strings.Fields(rest)
+	if len(valFields) == 0 {
+		return "", nil, 0, fmt.Errorf("missing metric value")
+	}
+	value, err = strconv.ParseFloat(valFields[0], 64)
+	if err != nil {
+		return "", nil, 0, err
+	}
+	return name, labels, value, nil
+}

+ 56 - 0
mtproto/manager_test.go

@@ -0,0 +1,56 @@
+package mtproto
+
+import (
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/database/model"
+)
+
+func TestParseMetricLine(t *testing.T) {
+	name, labels, val, err := parseMetricLine(`mtg_traffic{direction="to_client"} 12345`)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if name != "mtg_traffic" {
+		t.Fatalf("name=%q", name)
+	}
+	if labels["direction"] != "to_client" {
+		t.Fatalf("labels=%v", labels)
+	}
+	if val != 12345 {
+		t.Fatalf("val=%v", val)
+	}
+
+	name2, _, val2, err2 := parseMetricLine(`mtg_concurrency 7`)
+	if err2 != nil {
+		t.Fatal(err2)
+	}
+	if name2 != "mtg_concurrency" || val2 != 7 {
+		t.Fatalf("got %q %v", name2, val2)
+	}
+}
+
+func TestInstanceFromInbound(t *testing.T) {
+	ib := &model.Inbound{
+		Id:       3,
+		Tag:      "inbound-3",
+		Listen:   "0.0.0.0",
+		Port:     8443,
+		Protocol: model.MTProto,
+		Settings: `{"fakeTlsDomain":"example.com","secret":""}`,
+	}
+	inst, ok := InstanceFromInbound(ib)
+	if !ok {
+		t.Fatal("expected a usable instance")
+	}
+	if inst.Secret == "" {
+		t.Fatal("secret should be healed to a non-empty value")
+	}
+	if inst.Port != 8443 || inst.Id != 3 {
+		t.Fatalf("bad instance %+v", inst)
+	}
+
+	if _, ok := InstanceFromInbound(&model.Inbound{Protocol: model.VLESS}); ok {
+		t.Fatal("non-mtproto inbound should not produce an instance")
+	}
+}

+ 201 - 0
mtproto/process.go

@@ -0,0 +1,201 @@
+// Package mtproto manages mtg (github.com/9seconds/mtg) sidecar processes that
+// serve MTProto FakeTLS proxies. Xray-core has no mtproto protocol, so mtproto
+// inbounds are run as standalone mtg processes — one process per inbound —
+// entirely outside the Xray config and lifecycle.
+package mtproto
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"runtime"
+	"strings"
+	"sync"
+	"sync/atomic"
+	"syscall"
+	"time"
+
+	"github.com/mhsanaei/3x-ui/v3/config"
+	"github.com/mhsanaei/3x-ui/v3/logger"
+)
+
+// GetBinaryName returns the mtg binary filename for the current OS and arch,
+// matching the naming scheme used for the Xray binary. On Windows the ".exe"
+// extension is appended so a natural "mtg-windows-amd64.exe" is found.
+func GetBinaryName() string {
+	name := fmt.Sprintf("mtg-%s-%s", runtime.GOOS, runtime.GOARCH)
+	if runtime.GOOS == "windows" {
+		name += ".exe"
+	}
+	return name
+}
+
+// GetBinaryPath returns the full path to the mtg binary, alongside the Xray binary.
+func GetBinaryPath() string {
+	return config.GetBinFolderPath() + "/" + GetBinaryName()
+}
+
+func configDir() string {
+	return config.GetBinFolderPath() + "/mtproto"
+}
+
+func configPathForID(id int) string {
+	return fmt.Sprintf("%s/mtg-%d.toml", configDir(), id)
+}
+
+var (
+	gracefulStopTimeout = 5 * time.Second
+	forceStopTimeout    = 2 * time.Second
+)
+
+type lastLineWriter struct {
+	mu       sync.Mutex
+	lastLine string
+}
+
+func (w *lastLineWriter) Write(p []byte) (int, error) {
+	line := strings.TrimSpace(string(p))
+	if line != "" {
+		w.mu.Lock()
+		w.lastLine = line
+		w.mu.Unlock()
+	}
+	return len(p), nil
+}
+
+func (w *lastLineWriter) LastLine() string {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+	return w.lastLine
+}
+
+// Process wraps a single mtg process invocation for one mtproto inbound.
+type Process struct {
+	cmd             *exec.Cmd
+	done            chan struct{}
+	configPath      string
+	logWriter       *lastLineWriter
+	exitErr         error
+	intentionalStop atomic.Bool
+}
+
+func newProcess(configPath string) *Process {
+	return &Process{
+		configPath: configPath,
+		logWriter:  &lastLineWriter{},
+	}
+}
+
+// IsRunning reports whether the mtg process is currently running.
+func (p *Process) IsRunning() bool {
+	if p.cmd == nil || p.cmd.Process == nil {
+		return false
+	}
+	if p.done != nil {
+		select {
+		case <-p.done:
+			return false
+		default:
+		}
+	}
+	if p.cmd.ProcessState == nil {
+		return true
+	}
+	return false
+}
+
+// GetResult returns the last log line or the exit error from the mtg process.
+func (p *Process) GetResult() string {
+	if line := p.logWriter.LastLine(); line != "" {
+		return line
+	}
+	if p.exitErr != nil {
+		return p.exitErr.Error()
+	}
+	return ""
+}
+
+// Start launches the mtg process against its generated config file.
+func (p *Process) Start() error {
+	if p.IsRunning() {
+		return errors.New("mtg is already running")
+	}
+	cmd := exec.Command(GetBinaryPath(), "run", p.configPath)
+	cmd.Stdout = p.logWriter
+	cmd.Stderr = p.logWriter
+	p.cmd = cmd
+	p.done = make(chan struct{})
+	p.exitErr = nil
+	p.intentionalStop.Store(false)
+	if err := cmd.Start(); err != nil {
+		close(p.done)
+		p.cmd = nil
+		return err
+	}
+	attachChildLifetime(cmd)
+	go p.wait(cmd)
+	return nil
+}
+
+func (p *Process) wait(cmd *exec.Cmd) {
+	defer close(p.done)
+	err := cmd.Wait()
+	if err == nil || p.intentionalStop.Load() {
+		return
+	}
+	if runtime.GOOS == "windows" {
+		if strings.Contains(strings.ToLower(err.Error()), "exit status 1") {
+			p.exitErr = err
+			return
+		}
+	}
+	logger.Error("mtproto: mtg process exited:", err)
+	p.exitErr = err
+}
+
+// Stop terminates the running mtg process gracefully, falling back to a kill.
+func (p *Process) Stop() error {
+	if !p.IsRunning() {
+		return errors.New("mtg is not running")
+	}
+	p.intentionalStop.Store(true)
+
+	if runtime.GOOS == "windows" {
+		if err := p.cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
+			return err
+		}
+		return p.waitForExit(forceStopTimeout)
+	}
+
+	if err := p.cmd.Process.Signal(syscall.SIGTERM); err != nil {
+		if errors.Is(err, os.ErrProcessDone) {
+			return p.waitForExit(forceStopTimeout)
+		}
+		return err
+	}
+
+	if err := p.waitForExit(gracefulStopTimeout); err == nil {
+		return nil
+	}
+
+	logger.Warning("mtproto: mtg did not stop after SIGTERM, killing process")
+	if err := p.cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
+		return err
+	}
+	return p.waitForExit(forceStopTimeout)
+}
+
+func (p *Process) waitForExit(timeout time.Duration) error {
+	if p.done == nil {
+		return nil
+	}
+	timer := time.NewTimer(timeout)
+	defer timer.Stop()
+	select {
+	case <-p.done:
+		return nil
+	case <-timer.C:
+		return fmt.Errorf("timed out waiting for mtg process to stop after %s", timeout)
+	}
+}

+ 7 - 0
mtproto/process_other.go

@@ -0,0 +1,7 @@
+//go:build !windows
+
+package mtproto
+
+import "os/exec"
+
+func attachChildLifetime(_ *exec.Cmd) {}

+ 66 - 0
mtproto/process_windows.go

@@ -0,0 +1,66 @@
+//go:build windows
+
+package mtproto
+
+import (
+	"os/exec"
+	"sync"
+	"unsafe"
+
+	"github.com/mhsanaei/3x-ui/v3/logger"
+	"golang.org/x/sys/windows"
+)
+
+var (
+	killOnExitJobOnce sync.Once
+	killOnExitJob     windows.Handle
+	killOnExitJobErr  error
+)
+
+func ensureKillOnExitJob() (windows.Handle, error) {
+	killOnExitJobOnce.Do(func() {
+		h, err := windows.CreateJobObject(nil, nil)
+		if err != nil {
+			killOnExitJobErr = err
+			return
+		}
+		info := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{
+			BasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{
+				LimitFlags: windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
+			},
+		}
+		_, err = windows.SetInformationJobObject(
+			h,
+			windows.JobObjectExtendedLimitInformation,
+			uintptr(unsafe.Pointer(&info)),
+			uint32(unsafe.Sizeof(info)),
+		)
+		if err != nil {
+			windows.CloseHandle(h)
+			killOnExitJobErr = err
+			return
+		}
+		killOnExitJob = h
+	})
+	return killOnExitJob, killOnExitJobErr
+}
+
+func attachChildLifetime(cmd *exec.Cmd) {
+	if cmd == nil || cmd.Process == nil {
+		return
+	}
+	job, err := ensureKillOnExitJob()
+	if err != nil {
+		logger.Warning("mtproto: kill-on-exit job unavailable:", err)
+		return
+	}
+	h, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(cmd.Process.Pid))
+	if err != nil {
+		logger.Warning("mtproto: OpenProcess for job attach failed:", err)
+		return
+	}
+	defer windows.CloseHandle(h)
+	if err := windows.AssignProcessToJobObject(job, h); err != nil {
+		logger.Warning("mtproto: AssignProcessToJobObject failed:", err)
+	}
+}

+ 12 - 5
sub/subClashService.go

@@ -221,8 +221,11 @@ func (s *SubClashService) buildProxy(inbound *model.Inbound, client model.Client
 		}
 		var inboundSettings map[string]any
 		json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
-		if encryption, ok := inboundSettings["encryption"].(string); ok && encryption != "" {
-			proxy["packet-encoding"] = encryption
+		if encryption, ok := inboundSettings["encryption"].(string); ok {
+			encryption = strings.TrimSpace(encryption)
+			if encryption != "" && encryption != "none" {
+				proxy["encryption"] = encryption
+			}
 		}
 	case model.Trojan:
 		proxy["type"] = "trojan"
@@ -573,10 +576,14 @@ func mergeClashRulesYAML(base map[string]any, raw string) error {
 	case []any:
 		mergeClashRules(base, typed)
 	case map[string]any:
-		if rules, ok := typed["rules"]; ok {
-			if ruleList, ok := asAnySlice(rules); ok {
-				mergeClashRules(base, ruleList)
+		for key, value := range typed {
+			if key == "rules" {
+				if ruleList, ok := asAnySlice(value); ok {
+					mergeClashRules(base, ruleList)
+				}
+				continue
 			}
+			base[key] = value
 		}
 	default:
 		mergeClashRules(base, linesToClashRules(raw))

+ 59 - 0
sub/subClashService_test.go

@@ -3,6 +3,8 @@ package sub
 import (
 	"reflect"
 	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/database/model"
 )
 
 func TestEnsureUniqueProxyNames(t *testing.T) {
@@ -113,3 +115,60 @@ func TestApplyTransport_HTTPUpgrade(t *testing.T) {
 		t.Fatalf("headers.Host = %v, want example.com", headers["Host"])
 	}
 }
+
+func TestBuildProxy_VLESSPostQuantumEncryptionUsesMihomoEncryptionField(t *testing.T) {
+	svc := &SubClashService{SubService: &SubService{remarkModel: "-i"}}
+	encryption := "mlkem768x25519plus.native.0rtt.client"
+	inbound := &model.Inbound{
+		Listen:   "203.0.113.1",
+		Port:     443,
+		Protocol: model.VLESS,
+		Remark:   "pq",
+		Settings: `{"encryption":"` + encryption + `"}`,
+	}
+	client := model.Client{ID: "11111111-2222-4333-8444-555555555555"}
+	stream := map[string]any{
+		"network": "xhttp",
+		"xhttpSettings": map[string]any{
+			"path": "/",
+			"mode": "auto",
+		},
+		"security": "reality",
+		"realitySettings": map[string]any{
+			"publicKey":  "pub",
+			"serverName": "example.com",
+			"shortId":    "abcd",
+		},
+	}
+
+	proxy := svc.buildProxy(inbound, client, stream, "")
+
+	if proxy["encryption"] != encryption {
+		t.Fatalf("encryption = %v, want %q", proxy["encryption"], encryption)
+	}
+}
+
+func TestBuildProxy_VLESSNoneEncryptionOmittedForClash(t *testing.T) {
+	svc := &SubClashService{SubService: &SubService{remarkModel: "-i"}}
+	inbound := &model.Inbound{
+		Listen:   "203.0.113.1",
+		Port:     443,
+		Protocol: model.VLESS,
+		Remark:   "plain",
+		Settings: `{"encryption":"none"}`,
+	}
+	client := model.Client{ID: "11111111-2222-4333-8444-555555555555"}
+	stream := map[string]any{
+		"network":  "tcp",
+		"security": "none",
+		"tcpSettings": map[string]any{
+			"header": map[string]any{"type": "none"},
+		},
+	}
+
+	proxy := svc.buildProxy(inbound, client, stream, "")
+
+	if _, ok := proxy["encryption"]; ok {
+		t.Fatalf("plain vless encryption should be omitted for mihomo: %#v", proxy)
+	}
+}

+ 32 - 3
sub/subService.go

@@ -374,10 +374,38 @@ func (s *SubService) GetLink(inbound *model.Inbound, email string) string {
 		return s.genShadowsocksLink(inbound, email)
 	case "hysteria":
 		return s.genHysteriaLink(inbound, email)
+	case "mtproto":
+		return s.genMtprotoLink(inbound, email)
 	}
 	return ""
 }
 
+// genMtprotoLink builds a Telegram proxy deep link for an mtproto inbound:
+// tg://proxy?server=<addr>&port=<port>&secret=<ee FakeTLS secret>.
+func (s *SubService) genMtprotoLink(inbound *model.Inbound, email string) string {
+	if inbound.Protocol != model.MTProto {
+		return ""
+	}
+	settings := map[string]any{}
+	json.Unmarshal([]byte(inbound.Settings), &settings)
+	secret, _ := settings["secret"].(string)
+	if secret == "" {
+		if healed, ok := model.HealMtprotoSecret(inbound.Settings); ok {
+			_ = json.Unmarshal([]byte(healed), &settings)
+			secret, _ = settings["secret"].(string)
+		}
+	}
+	if secret == "" {
+		return ""
+	}
+	params := map[string]string{
+		"server": s.resolveInboundAddress(inbound),
+		"port":   fmt.Sprintf("%d", inbound.Port),
+		"secret": secret,
+	}
+	return buildLinkWithParams("tg://proxy", params, s.genRemark(inbound, email, ""))
+}
+
 // Protocol link generators are intentionally ordered as:
 // vmess -> vless -> trojan -> shadowsocks -> hysteria.
 func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
@@ -743,9 +771,10 @@ func (s *SubService) loadNodes() {
 }
 
 // resolveInboundAddress picks the host an external client should connect to:
-//   1. node-managed inbound -> the node's address
-//   2. an explicit, client-reachable bind Listen -> that Listen
-//   3. otherwise the subscriber's request host (s.address)
+//  1. node-managed inbound -> the node's address
+//  2. an explicit, client-reachable bind Listen -> that Listen
+//  3. otherwise the subscriber's request host (s.address)
+//
 // A loopback/wildcard bind or a unix-domain-socket listen is a server-side
 // detail and is never advertised; External Proxy remains the way to advertise
 // an arbitrary endpoint. Mirrors the frontend's resolveAddr so the panel QR and

+ 39 - 2
update.sh

@@ -582,12 +582,14 @@ prompt_and_setup_ssl() {
     echo -e "${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)"
     echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)"
     echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)"
+    echo -e "${green}4.${plain} Skip SSL (advanced — behind reverse proxy / SSH tunnel only)"
     echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths."
+    echo -e "${blue}Note:${plain} Option 4 serves the panel over plain HTTP — only safe behind nginx/Caddy or an SSH tunnel."
     read -rp "Choose an option (default 2 for IP): " ssl_choice
     ssl_choice="${ssl_choice// /}" # Trim whitespace
 
-    # Default to 2 (IP cert) if input is empty or invalid (not 1 or 3)
-    if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
+    # Default to 2 (IP cert) if input is empty or invalid (not 1, 3 or 4)
+    if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" && "$ssl_choice" != "4" ]]; then
         ssl_choice="2"
     fi
 
@@ -706,6 +708,41 @@ prompt_and_setup_ssl() {
 
             systemctl restart x-ui > /dev/null 2>&1 || rc-service x-ui restart > /dev/null 2>&1
             ;;
+        4)
+            echo ""
+            echo -e "${red}⚠ Panel will be installed WITHOUT SSL/TLS.${plain}"
+            echo -e "${yellow}Login credentials and cookies will travel as plain HTTP.${plain}"
+            echo -e "${yellow}Only safe when:${plain}"
+            echo -e "${yellow}  • A reverse proxy (nginx, Caddy, Traefik) terminates TLS for you, or${plain}"
+            echo -e "${yellow}  • You access the panel exclusively via SSH tunnel${plain}"
+            echo ""
+
+            SSL_SCHEME="http"
+            SSL_HOST="${server_ip}"
+
+            local bind_local=""
+            read -rp "Bind the panel to 127.0.0.1 only? (recommended — forces SSH tunnel / reverse-proxy access) [y/N]: " bind_local
+            if [[ "$bind_local" == "y" || "$bind_local" == "Y" ]]; then
+                ${xui_folder}/x-ui setting -listenIP "127.0.0.1" > /dev/null 2>&1
+                SSL_HOST="127.0.0.1"
+                echo -e "${green}✓ Panel bound to 127.0.0.1 only. It is now unreachable from the public internet.${plain}"
+                echo ""
+                echo -e "${green}SSH Port Forwarding — open the panel from your local machine via:${plain}"
+                echo -e "  Standard SSH command:"
+                echo -e "  ${yellow}ssh -L 2222:127.0.0.1:${panel_port} root@${server_ip}${plain}"
+                echo -e "  If using an SSH key:"
+                echo -e "  ${yellow}ssh -i <sshkeypath> -L 2222:127.0.0.1:${panel_port} root@${server_ip}${plain}"
+                echo -e "  Then open in your browser:"
+                echo -e "  ${yellow}http://localhost:2222/${web_base_path}${plain}"
+                echo ""
+                echo -e "${yellow}Alternative: point a reverse proxy (nginx/Caddy) at 127.0.0.1:${panel_port} and let it terminate TLS.${plain}"
+            else
+                echo -e "${yellow}Panel will listen on all interfaces over plain HTTP. Make sure something else is terminating TLS in front of it.${plain}"
+            fi
+
+            systemctl restart x-ui > /dev/null 2>&1 || rc-service x-ui restart > /dev/null 2>&1
+            echo -e "${green}✓ SSL setup skipped.${plain}"
+            ;;
         *)
             echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
             SSL_HOST="${server_ip}"

+ 62 - 0
web/job/mtproto_job.go

@@ -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)
+	}
+}

+ 18 - 0
web/runtime/local.go

@@ -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)
 	})

+ 4 - 1
web/service/api_scale_postgres_test.go

@@ -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

+ 55 - 1
web/service/inbound.go

@@ -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 {
@@ -1828,7 +1841,14 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
 			// from the node arriving after a central disable would otherwise
 			// overwrite enable=false back to true, letting the client accumulate
 			// far more traffic than their limit before being disabled again.
-			enableExpr := "enable AND ?"
+			//
+			// We use a dialect-aware expression (see database.ClientTrafficEnableMergeExpr)
+			// because the old "enable AND ?" form (and naive CASE with :: casts)
+			// caused type mismatches on PostgreSQL after public API inbound updates
+			// (which go through updateClientTraffics + SyncInbound and can touch
+			// client_traffics rows) and would also break on SQLite due to PG-only
+			// ::boolean syntax.
+			enableExpr := database.ClientTrafficEnableMergeExpr()
 			if err := tx.Exec(
 				fmt.Sprintf(
 					`UPDATE client_traffics
@@ -3543,6 +3563,40 @@ func (s *InboundService) MigrationRequirements() {
 		}
 	}
 
+	// Normalize "enable" columns to boolean on Postgres. Legacy SQLite data
+	// (0/1 integers), partial migrations, or mixed write paths (public API
+	// inbound updates that flow through UpdateClientStat + client syncs, plus
+	// node traffic merge deltas) can leave the column as integer or with mixed
+	// interpretation. This (combined with the dialect-aware
+	// ClientTrafficEnableMergeExpr) prevents type problems in the node traffic
+	// sync merge (SetRemoteTraffic) and makes the sync robust even when
+	// inbounds are updated via the public API (incl. ones carrying
+	// externalProxy in streamSettings). The same expression is also safe on
+	// SQLite (no PG :: casts).
+	if database.IsPostgres() {
+		// Use DO block so it is idempotent and doesn't fail if already boolean.
+		normalizeBool := func(table, col string) {
+			tx.Exec(fmt.Sprintf(`
+				DO $$
+				BEGIN
+					IF EXISTS (
+						SELECT 1 FROM information_schema.columns
+						WHERE table_name = '%s' AND column_name = '%s'
+						  AND data_type <> 'boolean'
+					) THEN
+						ALTER TABLE %s ALTER COLUMN %s
+							TYPE boolean USING (CASE WHEN %s::text IN ('1','true','t','yes') THEN true ELSE false END);
+					END IF;
+				END $$;`, table, col, table, col, col))
+		}
+		normalizeBool("inbounds", "enable")
+		normalizeBool("client_traffics", "enable")
+		normalizeBool("nodes", "enable")
+		normalizeBool("clients", "enable")
+		normalizeBool("api_tokens", "enabled")
+		normalizeBool("outbound_subscriptions", "enabled")
+	}
+
 	// Fix inbounds based problems
 	var inbounds []*model.Inbound
 	err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan", "shadowsocks", "hysteria"}).Find(&inbounds).Error

+ 2 - 0
web/service/port_conflict.go

@@ -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

+ 3 - 0
web/service/xray.go

@@ -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)
 

+ 3 - 0
web/translation/ar-EG.json

@@ -501,6 +501,9 @@
         "accounts": "الحسابات",
         "allowTransparent": "السماح بالشفاف",
         "encryptionMethod": "طريقة التشفير",
+        "fakeTlsDomain": "نطاق FakeTLS (SNI)",
+        "mtprotoSecret": "المفتاح السري",
+        "mtprotoHint": "يتم تقديم MTProto عبر عملية mtg منفصلة وليس Xray. إعدادات النقل والعملاء لا تنطبق هنا — شارك الرابط أدناه مع تيليجرام.",
         "visionTestseed": "Vision testseed",
         "version": "الإصدار",
         "udpIdleTimeout": "UDP idle timeout (ثانية)",

+ 3 - 0
web/translation/en-US.json

@@ -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)",

+ 3 - 0
web/translation/es-ES.json

@@ -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)",

+ 3 - 0
web/translation/fa-IR.json

@@ -501,6 +501,9 @@
         "accounts": "حساب‌ها",
         "allowTransparent": "اجازه شفاف",
         "encryptionMethod": "روش رمزنگاری",
+        "fakeTlsDomain": "دامنه FakeTLS (SNI)",
+        "mtprotoSecret": "کلید مخفی",
+        "mtprotoHint": "پروتکل MTProto توسط یک پردازش جداگانه mtg ارائه می‌شود، نه Xray. تنظیمات انتقال و کلاینت‌ها اینجا کاربرد ندارند — لینک زیر را با تلگرام به اشتراک بگذارید.",
         "visionTestseed": "Vision testseed",
         "version": "نسخه",
         "udpIdleTimeout": "UDP idle timeout (s)",

+ 3 - 0
web/translation/id-ID.json

@@ -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)",

+ 3 - 0
web/translation/ja-JP.json

@@ -501,6 +501,9 @@
         "accounts": "アカウント",
         "allowTransparent": "透過を許可",
         "encryptionMethod": "暗号化方式",
+        "fakeTlsDomain": "FakeTLS ドメイン (SNI)",
+        "mtprotoSecret": "シークレット",
+        "mtprotoHint": "MTProto は Xray ではなく独立した mtg プロセスで提供されます。ストリーム設定とクライアントはここでは適用されません。下のリンクを Telegram で共有してください。",
         "visionTestseed": "Vision testseed",
         "version": "バージョン",
         "udpIdleTimeout": "UDP idle timeout (秒)",

+ 3 - 0
web/translation/pt-BR.json

@@ -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)",

+ 3 - 0
web/translation/ru-RU.json

@@ -501,6 +501,9 @@
         "accounts": "Аккаунты",
         "allowTransparent": "Разрешить прозрачный",
         "encryptionMethod": "Метод шифрования",
+        "fakeTlsDomain": "Домен FakeTLS (SNI)",
+        "mtprotoSecret": "Секрет",
+        "mtprotoHint": "MTProto обслуживается отдельным процессом mtg, а не Xray. Настройки транспорта и клиенты здесь не применяются — поделитесь ссылкой ниже в Telegram.",
         "visionTestseed": "Vision testseed",
         "version": "Версия",
         "udpIdleTimeout": "UDP idle timeout (с)",

+ 215 - 212
web/translation/tr-TR.json

@@ -36,19 +36,19 @@
   "unlimited": "Sınırsız",
   "none": "Yok",
   "qrCode": "QR Kod",
-  "info": "Daha Fazla Bilgi",
+  "info": "Bilgi",
   "edit": "Düzenle",
   "delete": "Sil",
   "reset": "Sıfırla",
   "noData": "Veri yok.",
   "copySuccess": "Başarıyla Kopyalandı",
-  "sure": "Emin misiniz",
+  "sure": "Emin misiniz?",
   "encryption": "Şifreleme",
   "useIPv4ForHost": "Ana bilgisayar için IPv4 kullan",
   "transmission": "İletim",
   "host": "Host",
   "path": "Yol",
-  "camouflage": "Karartma",
+  "camouflage": "Maskeleme",
   "status": "Durum",
   "enabled": "Etkin",
   "disabled": "Devre Dışı",
@@ -62,10 +62,10 @@
   "fail": "Başarısız",
   "comment": "Yorum",
   "success": "Başarılı",
-  "lastOnline": "Son çevrimiçi",
+  "lastOnline": "Son Çevrimiçi",
   "getVersion": "Sürümü Al",
   "install": "Yükle",
-  "clients": "Müşteriler",
+  "clients": "Kullanıcılar",
   "usage": "Kullanım",
   "twoFactorCode": "Kod",
   "remained": "Kalan",
@@ -102,8 +102,8 @@
     "dark": "Koyu",
     "ultraDark": "Ultra Koyu",
     "dashboard": "Genel Bakış",
-    "inbounds": "Gelenler",
-    "clients": "İstemciler",
+    "inbounds": "Bağlantı Noktaları",
+    "clients": "Kullanıcılar",
     "groups": "Gruplar",
     "nodes": "Düğümler",
     "settings": "Panel Ayarları",
@@ -160,7 +160,7 @@
       "historyTitleNetwork": "Ağ Bant Genişliği",
       "historyTitlePackets": "Ağ Paketleri",
       "historyTitleDisk": "Disk G/Ç",
-      "historyTitleOnline": "Çevrimiçi İstemciler",
+      "historyTitleOnline": "Çevrimiçi Kullanıcılar",
       "historyTitleLoad": "Sistem Yük Ortalaması (1d / 5d / 15d)",
       "historyTitleConnections": "Etkin Bağlantılar (TCP / UDP)",
       "historyTitleDiskUsage": "Disk Alanı Kullanımı",
@@ -283,10 +283,10 @@
       "migrationDownloadPgDesc": "PostgreSQL verilerinizden oluşturulan ve bu paneli SQLite üzerinde çalıştırmaya hazır bir .db SQLite veritabanı indirmek için tıklayın."
     },
     "inbounds": {
-      "title": "Gelenler",
+      "title": "Bağlantı Noktaları",
       "totalDownUp": "Toplam Gönderilen/Alınan",
       "totalUsage": "Toplam Kullanım",
-      "inboundCount": "Toplam Gelen",
+      "inboundCount": "Toplam Bağlantı Noktası",
       "operate": "Menü",
       "enable": "Etkin",
       "remark": "Açıklama",
@@ -316,61 +316,61 @@
       "portMap": "Port eşlemesi",
       "traffic": "Trafik",
       "details": "Detaylar",
-      "transportConfig": "Taşıma",
+      "transportConfig": "Aktarım",
       "expireDate": "Süre",
       "createdAt": "Oluşturuldu",
       "updatedAt": "Güncellendi",
       "resetTraffic": "Trafiği sıfırla",
-      "addInbound": "Gelen Ekle",
+      "addInbound": "Bağlantı Noktası Ekle",
       "generalActions": "Genel Eylemler",
-      "modifyInbound": "Geleni Düzenle",
-      "deleteInbound": "Geleni Sil",
-      "deleteInboundContent": "Geleni silmek istediğinizden emin misiniz?",
+      "modifyInbound": "Bağlantı Noktasını Düzenle",
+      "deleteInbound": "Bağlantı Noktasını Sil",
+      "deleteInboundContent": "Bağlantı noktasını silmek istediğinizden emin misiniz?",
       "deleteConfirmTitle": "\"{remark}\" inbound silinsin mi?",
-      "deleteConfirmContent": "Bu işlem inbound'u ve tüm istemcilerini siler. Geri alınamaz.",
+      "deleteConfirmContent": "Bu işlem inbound'u ve tüm kullanıcılarıni siler. Geri alınamaz.",
       "resetConfirmTitle": "\"{remark}\" trafiği sıfırlansın mı?",
       "resetConfirmContent": "Bu inbound için gönderme/alma sayaçlarını 0'a sıfırlar.",
       "selectedCount": "{count} seçildi",
       "selectAll": "Tümünü seç",
       "bulkDeleteConfirmTitle": "{count} inbound silinsin mi?",
-      "bulkDeleteConfirmContent": "Bu işlem seçili inbound'ları ve tüm istemcilerini siler. Geri alınamaz.",
+      "bulkDeleteConfirmContent": "Bu işlem seçili inbound'ları ve tüm kullanıcılarıni siler. Geri alınamaz.",
       "cloneConfirmTitle": "\"{remark}\" inbound klonlansın mı?",
-      "cloneConfirmContent": "Yeni bir port ve boş istemci listesiyle bir kopya oluşturur.",
-      "delAllClients": "Tüm istemcileri sil",
-      "delAllClientsConfirmTitle": "\"{remark}\" içindeki {count} istemcinin tamamı silinsin mi?",
-      "delAllClientsConfirmContent": "Bu inbound'a ait tüm istemcileri ve trafik kayıtlarını siler. Inbound'un kendisi korunur. Bu işlem geri alınamaz.",
-      "attachClients": "İstemcileri şuna bağla…",
-      "addClientsToGroup": "İstemcileri gruba ekle…",
-      "attachClientsTitle": "«{remark}» gelenindeki istemcileri bağla",
-      "attachClientsDesc": "Aynı {count} istemciyi (aynı UUID/parola ve paylaşılan trafik) seçilen gelenlere bağlar. Bu gelende de kalırlar.",
-      "attachClientsTargets": "Hedef gelenler",
-      "attachClientsNoTargets": "Bağlanacak uyumlu başka gelen yok.",
+      "cloneConfirmContent": "Yeni bir port ve boş kullanıcı listesiyle bir kopya oluşturur.",
+      "delAllClients": "Tüm kullanıcıları sil",
+      "delAllClientsConfirmTitle": "\"{remark}\" içindeki {count} kullanıcının tamamı silinsin mi?",
+      "delAllClientsConfirmContent": "Bu inbound'a ait tüm kullanıcıları ve trafik kayıtlarını siler. Inbound'un kendisi korunur. Bu işlem geri alınamaz.",
+      "attachClients": "Kullanıcıları şuna bağla…",
+      "addClientsToGroup": "Kullanıcıları gruba ekle…",
+      "attachClientsTitle": "«{remark}» bağlantı noktasındaki kullanıcıları bağla",
+      "attachClientsDesc": "Aynı {count} kullanıcıyı (aynı UUID/parola ve paylaşılan trafik) seçilen bağlantı noktalarıe bağlar. Bu bağlantı noktasında de kalırlar.",
+      "attachClientsTargets": "Hedef bağlantı noktaları",
+      "attachClientsNoTargets": "Bağlanacak uyumlu başka bağlantı noktası yok.",
       "attachClientsResult": "Bağlandı {attached}, atlandı {skipped}.",
       "attachClientsResultMixed": "Bağlandı {attached}, atlandı {skipped}, hata {errors}.",
-      "attachClientsSelectLabel": "Bağlanacak istemciler",
+      "attachClientsSelectLabel": "Bağlanacak kullanıcılar",
       "attachClientsSearchPlaceholder": "Email veya yorum ara",
       "attachClientsStatusDisabled": "Devre dışı",
       "attachClientsSelectedCount": "{total} içinden {selected} seçildi",
-      "attachExistingClients": "Mevcut istemcileri bağla…",
-      "attachExistingTitle": "«{remark}» gelenine mevcut istemcileri bağla",
-      "attachExistingDesc": "Mevcut istemcileri ({count} uygun) bu gelene bağlar — aynı UUID/parola ve paylaşılan trafik. Zaten bu gelende olan istemciler atlanır.",
-      "attachExistingNoClients": "Henüz istemci yok. Önce istemci oluşturun, ardından buraya bağlayın.",
+      "attachExistingClients": "Mevcut kullanıcıları bağla…",
+      "attachExistingTitle": "«{remark}» bağlantı noktasına mevcut kullanıcıları bağla",
+      "attachExistingDesc": "Mevcut kullanıcıları ({count} uygun) bu bağlantı noktasına bağlar — aynı UUID/parola ve paylaşılan trafik. Zaten bu bağlantı noktasında olan kullanıcılar atlanır.",
+      "attachExistingNoClients": "Henüz kullanıcı yok. Önce kullanıcı oluşturun, ardından buraya bağlayın.",
       "attachExistingStatusAttached": "Zaten bağlı",
-      "detachClients": "İstemcileri çöz",
-      "detachClientsTitle": "«{remark}» gelenindeki istemcileri çöz",
-      "detachClientsDesc": "Seçilen istemcileri yalnızca bu gelenden kaldırır. İstemci kayıtları korunur (tamamen kaldırmak için Delete kullanın). Kaynakta toplam {count} istemci var.",
+      "detachClients": "Kullanıcıları çöz",
+      "detachClientsTitle": "«{remark}» bağlantı noktasındaki kullanıcıları çöz",
+      "detachClientsDesc": "Seçilen kullanıcıları yalnızca bu bağlantı noktasından kaldırır. Kullanıcı kayıtları korunur (tamamen kaldırmak için Delete kullanın). Kaynakta toplam {count} kullanıcı var.",
       "detachClientsResult": "Çözüldü {detached}, atlandı {skipped}.",
       "detachClientsResultMixed": "Çözüldü {detached}, atlandı {skipped}, hata {errors}.",
-      "detachClientsSelectLabel": "Çözülecek istemciler",
+      "detachClientsSelectLabel": "Çözülecek kullanıcılar",
       "exportLinksTitle": "Inbound bağlantılarını dışa aktar",
       "exportSubsTitle": "Abonelik bağlantılarını dışa aktar",
       "exportAllLinksTitle": "Tüm inbound bağlantılarını dışa aktar",
       "exportAllSubsTitle": "Tüm abonelik bağlantılarını dışa aktar",
-      "exportAllLinksFileName": "Tum-Gelenler",
-      "exportAllSubsFileName": "Tum-Gelenler-Subs",
-      "inboundJsonTitle": "Gelen JSON",
-      "deleteClient": "Müşteriyi Sil",
-      "deleteClientContent": "Müşteriyi silmek istediğinizden emin misiniz?",
+      "exportAllLinksFileName": "Tum-Bağlantı Noktaları",
+      "exportAllSubsFileName": "Tum-Bağlantı Noktaları-Subs",
+      "inboundJsonTitle": "Bağlantı Noktası JSON",
+      "deleteClient": "Kullanıcıyı Sil",
+      "deleteClientContent": "Kullanıcıyı silmek istediğinizden emin misiniz?",
       "resetTrafficContent": "Trafiği sıfırlamak istediğinizden emin misiniz?",
       "copyLink": "URL'yi Kopyala",
       "address": "Adres",
@@ -387,28 +387,28 @@
       "publicKey": "Genel Anahtar",
       "privatekey": "Özel Anahtar",
       "clickOnQRcode": "Kopyalamak için QR Kodu Tıklayın",
-      "client": "Müşteri",
+      "client": "Kullanıcı",
       "export": "Tüm URL'leri Dışa Aktar",
       "clone": "Klonla",
       "cloneInbound": "Klonla",
-      "cloneInboundContent": "Bu gelenin tüm ayarları, Port, Dinleme IP ve Müşteriler hariç, klona uygulanacaktır.",
+      "cloneInboundContent": "Bu bağlantı noktasının tüm ayarları, Port, Dinleme IP ve Kullanıcılar hariç, klona uygulanacaktır.",
       "cloneInboundOk": "Klonla",
       "resetAllTraffic": "Tüm Gelen Trafiğini Sıfırla",
       "resetAllTrafficTitle": "Tüm Gelen Trafiğini Sıfırla",
-      "resetAllTrafficContent": "Tüm gelenlerin trafiğini sıfırlamak istediğinizden emin misiniz?",
-      "resetInboundClientTraffics": "Müşteri Trafiklerini Sıfırla",
-      "resetInboundClientTrafficTitle": "Müşteri Trafiklerini Sıfırla",
-      "resetInboundClientTrafficContent": "Bu gelenin müşterilerinin trafiğini sıfırlamak istediğinizden emin misiniz?",
-      "resetAllClientTraffics": "Tüm Müşteri Trafiklerini Sıfırla",
-      "resetAllClientTrafficTitle": "Tüm Müşteri Trafiklerini Sıfırla",
-      "resetAllClientTrafficContent": "Tüm müşterilerin trafiğini sıfırlamak istediğinizden emin misiniz?",
-      "delDepletedClients": "Bitmiş Müşterileri Sil",
-      "delDepletedClientsTitle": "Bitmiş Müşterileri Sil",
-      "delDepletedClientsContent": "Tüm bitmiş müşterileri silmek istediğinizden emin misiniz?",
+      "resetAllTrafficContent": "Tüm bağlantı noktalarıin trafiğini sıfırlamak istediğinizden emin misiniz?",
+      "resetInboundClientTraffics": "Kullanıcı Trafiklerini Sıfırla",
+      "resetInboundClientTrafficTitle": "Kullanıcı Trafiklerini Sıfırla",
+      "resetInboundClientTrafficContent": "Bu bağlantı noktasının kullanıcılarınin trafiğini sıfırlamak istediğinizden emin misiniz?",
+      "resetAllClientTraffics": "Tüm Kullanıcı Trafiklerini Sıfırla",
+      "resetAllClientTrafficTitle": "Tüm Kullanıcı Trafiklerini Sıfırla",
+      "resetAllClientTrafficContent": "Tüm kullanıcıların trafiğini sıfırlamak istediğinizden emin misiniz?",
+      "delDepletedClients": "Bitmiş Kullanıcıları Sil",
+      "delDepletedClientsTitle": "Bitmiş Kullanıcıları Sil",
+      "delDepletedClientsContent": "Tüm bitmiş kullanıcıları silmek istediğinizden emin misiniz?",
       "email": "Email",
       "emailDesc": "Lütfen benzersiz bir e-posta adresi sağlayın.",
       "IPLimit": "IP Limiti",
-      "IPLimitDesc": "Sayının aşılması durumunda gelen devre dışı bırakılır. (0 = devre dışı)",
+      "IPLimitDesc": "Sayının aşılması durumunda bağlantı noktası devre dışı bırakılır. (0 = devre dışı)",
       "IPLimitlog": "IP Günlüğü",
       "IPLimitlogDesc": "IP geçmiş günlüğü. (devre dışı bırakıldıktan sonra gelini etkinleştirmek için günlüğü temizleyin)",
       "IPLimitlogclear": "Günlüğü Temizle",
@@ -441,12 +441,12 @@
         "jsonErrorPrefix": "Gelişmiş JSON"
       },
       "telegramDesc": "Lütfen Telegram Sohbet Kimliği sağlayın. (botta '/id' komutunu kullanın) veya ({'@'}userinfobot)",
-      "subscriptionDesc": "Abonelik URL'inizi bulmak için 'Detaylar'a gidin. Ayrıca, aynı adı birden fazla müşteri için kullanabilirsiniz.",
+      "subscriptionDesc": "Abonelik URL'inizi bulmak için 'Detaylar'a gidin. Ayrıca, aynı adı birden fazla kullanıcı için kullanabilirsiniz.",
       "same": "Aynı",
-      "inboundInfo": "Gelen Bilgileri",
-      "exportInbound": "Geleni Dışa Aktar",
+      "inboundInfo": "Bağlantı Noktası Bilgileri",
+      "exportInbound": "Bağlantı Noktasını Dışa Aktar",
       "import": "İçe Aktar",
-      "importInbound": "Bir Gelen İçe Aktar",
+      "importInbound": "Bağlantı Noktası İçe Aktar",
       "periodicTrafficResetTitle": "Trafik Sıfırlama",
       "periodicTrafficResetDesc": "Belirtilen aralıklarla trafik sayacını otomatik olarak sıfırla",
       "lastReset": "Son Sıfırlama",
@@ -461,17 +461,17 @@
         "obtain": "Elde Et",
         "updateSuccess": "Güncelleme başarılı oldu",
         "logCleanSuccess": "Günlük temizlendi",
-        "inboundsUpdateSuccess": "Gelen bağlantılar başarıyla güncellendi",
-        "inboundUpdateSuccess": "Gelen bağlantı başarıyla güncellendi",
-        "inboundCreateSuccess": "Gelen bağlantı başarıyla oluşturuldu",
+        "inboundsUpdateSuccess": "Bağlantı noktaları başarıyla güncellendi",
+        "inboundUpdateSuccess": "Bağlantı noktası başarıyla güncellendi",
+        "inboundCreateSuccess": "Bağlantı noktası başarıyla oluşturuldu",
         "bulkDeleted": "{count} inbound silindi",
         "bulkDeletedMixed": "{ok} silindi, {failed} başarısız",
-        "inboundDeleteSuccess": "Gelen bağlantı başarıyla silindi",
-        "inboundClientAddSuccess": "Gelen bağlantı istemci(leri) eklendi",
-        "inboundClientDeleteSuccess": "Gelen bağlantı istemcisi silindi",
-        "inboundClientUpdateSuccess": "Gelen bağlantı istemcisi güncellendi",
-        "delDepletedClientsSuccess": "Tüm tükenmiş istemciler silindi",
-        "resetAllClientTrafficSuccess": "İstemcinin tüm trafiği sıfırlandı",
+        "inboundDeleteSuccess": "Bağlantı noktası başarıyla silindi",
+        "inboundClientAddSuccess": "Bağlantı noktası kullanıcı(leri) eklendi",
+        "inboundClientDeleteSuccess": "Bağlantı noktası kullanıcısı silindi",
+        "inboundClientUpdateSuccess": "Bağlantı noktası kullanıcısı güncellendi",
+        "delDepletedClientsSuccess": "Tüm tükenmiş kullanıcılar silindi",
+        "resetAllClientTrafficSuccess": "Kullanıcının tüm trafiği sıfırlandı",
         "resetAllTrafficSuccess": "Tüm trafik sıfırlandı",
         "resetInboundClientTrafficSuccess": "Trafik sıfırlandı",
         "resetInboundTrafficSuccess": "Gelen trafik sıfırlandı",
@@ -479,7 +479,7 @@
         "getNewX25519CertError": "X25519 sertifikası alınırken hata oluştu.",
         "getNewmldsa65Error": "mldsa65 sertifikası alınırken hata oluştu.",
         "getNewVlessEncError": "VlessEnc sertifikası alınırken hata oluştu.",
-        "invalidClientField": "Müşteri {client}: alan {field} — {reason}",
+        "invalidClientField": "Kullanıcı {client}: alan {field} — {reason}",
         "invalidField": "{field} — {reason}",
         "moreIssues": "{message}  (+{count} tane daha)"
       },
@@ -487,7 +487,7 @@
         "moveUp": "Yukarı",
         "moveDown": "Aşağı",
         "addAll": "Tümünü ekle",
-        "addAllFallbackTooltip": "Henüz bağlanmamış her uygun gelen için bir fallback satırı ekler",
+        "addAllFallbackTooltip": "Henüz bağlanmamış her uygun bağlantı noktası için bir fallback satırı ekler",
         "peers": "Peers",
         "addPeer": "Peer ekle",
         "keepAlive": "Keep-alive",
@@ -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)",
@@ -587,7 +590,7 @@
         "echKey": "ECH key",
         "echConfig": "ECH yapılandırması",
         "pinnedPeerCertSha256": "Sabitlenmiş Peer Sertifikası SHA-256",
-        "pinnedPeerCertSha256Tip": "Peer sertifikasının SHA-256 hash'leri onaltılık (hex) dizge olarak (örn. e8e2d3…), virgülle ayrılmış. Sadece panel — sunucunun xray yapılandırmasına yazılmaz, ancak istemcilerin sertifikayı sabitleyebilmesi için paylaşım bağlantılarına eklenir.",
+        "pinnedPeerCertSha256Tip": "Peer sertifikasının SHA-256 hash'leri onaltılık (hex) dizge olarak (örn. e8e2d3…), virgülle ayrılmış. Sadece panel — sunucunun xray yapılandırmasına yazılmaz, ancak kullanıcıların sertifikayı sabitleyebilmesi için paylaşım bağlantılarına eklenir.",
         "pinnedPeerCertSha256Placeholder": "onaltılık (hex) hash(ler), virgülle ayrılmış",
         "generateRandomPin": "Rastgele hash üret",
         "getNewEchCert": "Yeni ECH sertifikası al",
@@ -595,8 +598,8 @@
         "xver": "Xver",
         "target": "Hedef",
         "maxTimeDiff": "Maks. zaman farkı (ms)",
-        "minClientVer": "Min. istemci sürümü",
-        "maxClientVer": "Maks. istemci sürümü",
+        "minClientVer": "Min. kullanıcı sürümü",
+        "maxClientVer": "Maks. kullanıcı sürümü",
         "shortIds": "Short IDs",
         "realityTargetHint": "Zorunlu. Bir bağlantı noktası içermeli (ör. example.com:443). Bağlantı noktası olmadan Xray-core başlamaz.",
         "realityTargetRequired": "REALITY hedefi zorunludur",
@@ -645,24 +648,24 @@
       }
     },
     "clients": {
-      "add": "İstemci ekle",
-      "edit": "İstemciyi düzenle",
-      "submitAdd": "İstemci ekle",
+      "add": "Kullanıcı ekle",
+      "edit": "Kullanıcıyı düzenle",
+      "submitAdd": "Kullanıcı ekle",
       "submitEdit": "Değişiklikleri kaydet",
-      "clientCount": "İstemci sayısı",
+      "clientCount": "Kullanıcı sayısı",
       "bulk": "Toplu ekle",
-      "copyFromInbound": "Inbound'dan istemcileri kopyala",
-      "copyToInbound": "İstemcileri kopyalanacak yer",
+      "copyFromInbound": "Inbound'dan kullanıcıları kopyala",
+      "copyToInbound": "Kullanıcıları kopyalanacak yer",
       "copySelected": "Seçileni kopyala",
       "copySource": "Kaynak",
       "copyEmailPreview": "Oluşacak e-posta önizlemesi",
       "copySelectSourceFirst": "Önce bir kaynak inbound seçin.",
       "copyResult": "Kopya sonucu",
       "copyResultSuccess": "Başarıyla kopyalandı",
-      "copyResultNone": "Kopyalanacak bir şey yok: istemci seçilmemiş veya kaynak boş",
+      "copyResultNone": "Kopyalanacak bir şey yok: kullanıcı seçilmemiş veya kaynak boş",
       "copyResultErrors": "Kopyalama hataları",
-      "copyFlowLabel": "Yeni istemciler için Flow (VLESS)",
-      "copyFlowHint": "Kopyalanan tüm istemcilere uygulanır. Atlamak için boş bırakın.",
+      "copyFlowLabel": "Yeni kullanıcılar için Flow (VLESS)",
+      "copyFlowHint": "Kopyalanan tüm kullanıcılara uygulanır. Atlamak için boş bırakın.",
       "selectAll": "Tümünü seç",
       "clearAll": "Tümünü temizle",
       "method": "Yöntem",
@@ -677,7 +680,7 @@
       "renew": "Otomatik yenileme",
       "renewDesc": "Süre dolduktan sonra otomatik yenileme. (0 = devre dışı) (birim: gün)",
       "searchPlaceholder": "Email, yorum, sub ID, UUID, parola, auth ara…",
-      "filterTitle": "İstemcileri filtrele",
+      "filterTitle": "Kullanıcıları filtrele",
       "clearAllFilters": "Tümünü temizle",
       "showingCount": "{total} içinden {shown} gösteriliyor",
       "sortOldest": "Önce en eski",
@@ -691,11 +694,11 @@
       "sortExpiringSoonest": "Yakında biten",
       "has": "Var",
       "hasNot": "Yok",
-      "title": "İstemciler",
+      "title": "Kullanıcılar",
       "actions": "Eylemler",
       "totalGB": "Toplam Gönderilen/Alınan (GB)",
       "expiryTime": "Son kullanma",
-      "addClients": "İstemci ekle",
+      "addClients": "Kullanıcı ekle",
       "limitIp": "IP limiti",
       "password": "Şifre",
       "subId": "Abonelik ID'si",
@@ -704,60 +707,60 @@
       "emailInvalidChars": "E-posta boşluk, '/', '\\' veya kontrol karakterleri içeremez",
       "subIdInvalidChars": "Abonelik kimliği boşluk, '/', '\\' veya kontrol karakterleri içeremez",
       "group": "Grup",
-      "groupDesc": "İlgili istemcileri gruplamak için mantıksal etiket (ekip, müşteri, bölge). Araç çubuğundan filtrelenebilir.",
+      "groupDesc": "İlgili kullanıcıları gruplamak için mantıksal etiket (ekip, kullanıcı, bölge). Araç çubuğundan filtrelenebilir.",
       "groupPlaceholder": "örn. customer-a",
       "comment": "Yorum",
       "traffic": "Trafik",
       "offline": "Çevrimdışı",
-      "addClient": "İstemci ekle",
+      "addClient": "Kullanıcı ekle",
       "qrCode": "QR kodu",
-      "clientInfo": "İstemci Bilgileri",
+      "clientInfo": "Kullanıcı Bilgileri",
       "delete": "Sil",
       "reset": "Trafiği sıfırla",
-      "editClient": "İstemciyi düzenle",
-      "client": "İstemci",
+      "editClient": "Kullanıcıyı düzenle",
+      "client": "Kullanıcı",
       "enabled": "Etkin",
       "remaining": "Kalan",
       "duration": "Süre",
       "attachedInbounds": "Bağlı inbound'lar",
       "selectInbound": "Bir veya daha fazla inbound seçin",
-      "noSubId": "Bu istemcinin subId'si yok, paylaşılabilir bağlantı yok.",
-      "noLinks": "Paylaşılabilir bağlantı yok — önce bu istemciyi protokol destekli bir inbound'a bağlayın.",
+      "noSubId": "Bu kullanıcının subId'si yok, paylaşılabilir bağlantı yok.",
+      "noLinks": "Paylaşılabilir bağlantı yok — önce bu kullanıcıyı protokol destekli bir inbound'a bağlayın.",
       "link": "Bağlantı",
-      "resetNotPossible": "Önce bu istemciyi bir inbound'a bağlayın.",
+      "resetNotPossible": "Önce bu kullanıcıyı bir inbound'a bağlayın.",
       "general": "Genel",
-      "resetAllTraffics": "Tüm istemcilerin trafiğini sıfırla",
-      "resetAllTrafficsTitle": "Tüm istemcilerin trafiği sıfırlansın mı?",
-      "resetAllTrafficsContent": "Her istemcinin yükleme/indirme sayaçları sıfırlanır. Kotalar ve son kullanma tarihleri etkilenmez. Geri alınamaz.",
-      "deleteConfirmTitle": "{email} istemcisi silinsin mi?",
-      "deleteConfirmContent": "Bu işlem istemciyi bağlı tüm inbound'lardan kaldırır ve trafik kaydını siler. Geri alınamaz.",
+      "resetAllTraffics": "Tüm kullanıcıların trafiğini sıfırla",
+      "resetAllTrafficsTitle": "Tüm kullanıcıların trafiği sıfırlansın mı?",
+      "resetAllTrafficsContent": "Her kullanıcının yükleme/indirme sayaçları sıfırlanır. Kotalar ve son kullanma tarihleri etkilenmez. Geri alınamaz.",
+      "deleteConfirmTitle": "{email} kullanıcısı silinsin mi?",
+      "deleteConfirmContent": "Bu işlem kullanıcıyı bağlı tüm inbound'lardan kaldırır ve trafik kaydını siler. Geri alınamaz.",
       "deleteSelected": "Sil ({count})",
       "adjustSelected": "Ayarla ({count})",
       "subLinksSelected": "Abonelik bağlantıları ({count})",
-      "addToGroupTitle": "{count} istemciyi bir gruba ekle",
-      "addToGroupTooltip": "Mevcut bir grubu seçin veya yeni ad girin. İstemcileri mevcut gruplarından çıkarmak için Ungroup'u kullanın.",
+      "addToGroupTitle": "{count} kullanıcıyı bir gruba ekle",
+      "addToGroupTooltip": "Mevcut bir grubu seçin veya yeni ad girin. Kullanıcıları mevcut gruplarından çıkarmak için Ungroup'u kullanın.",
       "groupName": "Grup adı",
-      "addToGroupSuccessToast": "{count} istemci {group} grubuna eklendi",
-      "ungroupSuccessToast": "{count} istemcinin grubu temizlendi",
+      "addToGroupSuccessToast": "{count} kullanıcı {group} grubuna eklendi",
+      "ungroupSuccessToast": "{count} kullanıcının grubu temizlendi",
       "ungroup": "Gruptan çıkar",
-      "ungroupConfirmTitle": "{count} istemciyi gruptan çıkar?",
-      "ungroupConfirmContent": "Seçilen her istemcinin grup etiketini temizler. İstemciler korunur (tamamen kaldırmak için Delete kullanın).",
+      "ungroupConfirmTitle": "{count} kullanıcıyı gruptan çıkar?",
+      "ungroupConfirmContent": "Seçilen her kullanıcının grup etiketini temizler. Kullanıcılar korunur (tamamen kaldırmak için Delete kullanın).",
       "addToGroup": "Gruba ekle",
       "attach": "Bağla",
       "adjust": "Ayarla",
       "subLinks": "Abonelik bağlantıları",
       "selectedCount": "{count} seçildi",
       "attachSelected": "Bağla ({count})",
-      "attachToInboundsTitle": "{count} istemciyi gelen(ler)e bağla",
-      "attachToInboundsDesc": "Seçilen {count} istemciyi (aynı UUID/parola ve paylaşılan trafik) seçilen gelene bağlar. Mevcut bağlantılar korunur.",
-      "attachToInboundsTargets": "Hedef gelenler",
-      "attachToInboundsNoTargets": "Bağlanacak çoklu kullanıcılı gelen yok.",
+      "attachToInboundsTitle": "{count} kullanıcıyı bağlantı nokta(ları)sına bağla",
+      "attachToInboundsDesc": "Seçilen {count} kullanıcıyı (aynı UUID/parola ve paylaşılan trafik) seçilen bağlantı noktasına bağlar. Mevcut bağlantılar korunur.",
+      "attachToInboundsTargets": "Hedef bağlantı noktaları",
+      "attachToInboundsNoTargets": "Bağlanacak çoklu kullanıcılı bağlantı noktası yok.",
       "detachSelected": "Çöz ({count})",
       "detach": "Çöz",
-      "detachFromInboundsTitle": "{count} istemciyi gelen(ler)den çöz",
-      "detachFromInboundsDesc": "Seçilen {count} istemciyi seçilen gelenden kaldırır. İstemcinin bağlı olmadığı çiftler sessizce atlanır. İstemci kayıtları korunur (tamamen kaldırmak için Delete kullanın).",
-      "detachFromInboundsTargets": "Çözülecek gelenler",
-      "detachFromInboundsNoTargets": "Çoklu kullanıcılı gelen yok.",
+      "detachFromInboundsTitle": "{count} kullanıcıyı bağlantı nokta(ları)sından çöz",
+      "detachFromInboundsDesc": "Seçilen {count} kullanıcıyı seçilen bağlantı noktasından kaldırır. Kullanıcının bağlı olmadığı çiftler sessizce atlanır. Kullanıcı kayıtları korunur (tamamen kaldırmak için Delete kullanın).",
+      "detachFromInboundsTargets": "Çözülecek bağlantı noktaları",
+      "detachFromInboundsNoTargets": "Çoklu kullanıcılı bağlantı noktası yok.",
       "detachFromInboundsResult": "Çözüldü {detached}, atlandı {skipped}.",
       "detachFromInboundsResultMixed": "Çözüldü {detached}, atlandı {skipped}, hata {errors}.",
       "subLinksTitle": "Abonelik bağlantıları ({count})",
@@ -765,19 +768,19 @@
       "subJsonLinkColumn": "Abonelik JSON URL",
       "subLinksCopyAll": "Tümünü kopyala",
       "subLinksCopiedAll": "{count} bağlantı kopyalandı",
-      "subLinksEmpty": "Seçilen istemcilerin hiçbirinin abonelik ID'si yok.",
+      "subLinksEmpty": "Seçilen kullanıcıların hiçbirinin abonelik ID'si yok.",
       "subLinksDisabled": "Abonelik hizmeti devre dışı.",
       "subLinksDisabledHint": "Bağlantı oluşturmak için Panel Ayarları → Abonelik'ten etkinleştirin.",
-      "bulkDeleteConfirmTitle": "{count} istemci silinsin mi?",
-      "bulkDeleteConfirmContent": "Seçili her istemci bağlı tüm inbound'lardan kaldırılır ve trafik kaydı silinir. Geri alınamaz.",
-      "bulkAdjustTitle": "{count} istemciyi ayarla",
-      "bulkAdjustHint": "Pozitif değerler ekler, negatif değerler azaltır. Sınırsız süreli veya trafikli istemciler ilgili alan için atlanır.",
+      "bulkDeleteConfirmTitle": "{count} kullanıcı silinsin mi?",
+      "bulkDeleteConfirmContent": "Seçili her kullanıcı bağlı tüm inbound'lardan kaldırılır ve trafik kaydı silinir. Geri alınamaz.",
+      "bulkAdjustTitle": "{count} kullanıcıyı ayarla",
+      "bulkAdjustHint": "Pozitif değerler ekler, negatif değerler azaltır. Sınırsız süreli veya trafikli kullanıcılar ilgili alan için atlanır.",
       "bulkAdjustNothing": "Uygulamadan önce gün veya trafik belirleyin.",
       "addDays": "Gün ekle",
       "addTrafficGB": "Trafik ekle (GB)",
       "delDepleted": "Tükenmişleri sil",
-      "delDepletedConfirmTitle": "Tükenmiş istemciler silinsin mi?",
-      "delDepletedConfirmContent": "Trafik kotası dolan veya süresi geçen tüm istemciler silinir. Geri alınamaz.",
+      "delDepletedConfirmTitle": "Tükenmiş kullanıcılar silinsin mi?",
+      "delDepletedConfirmContent": "Trafik kotası dolan veya süresi geçen tüm kullanıcılar silinir. Geri alınamaz.",
       "auth": "Auth",
       "hysteriaAuth": "Hysteria Auth",
       "uuid": "UUID",
@@ -791,55 +794,55 @@
       "updated": "Güncellendi",
       "ipLimit": "IP limiti",
       "toasts": {
-        "deleted": "İstemci silindi",
+        "deleted": "Kullanıcı silindi",
         "trafficReset": "Trafik sıfırlandı",
-        "allTrafficsReset": "Tüm istemcilerin trafiği sıfırlandı",
-        "bulkDeleted": "{count} istemci silindi",
+        "allTrafficsReset": "Tüm kullanıcıların trafiği sıfırlandı",
+        "bulkDeleted": "{count} kullanıcı silindi",
         "bulkDeletedMixed": "{ok} silindi, {failed} başarısız",
-        "bulkCreated": "{count} istemci oluşturuldu",
+        "bulkCreated": "{count} kullanıcı oluşturuldu",
         "bulkCreatedMixed": "{ok} oluşturuldu, {failed} başarısız",
-        "bulkAdjusted": "{count} istemci ayarlandı",
+        "bulkAdjusted": "{count} kullanıcı ayarlandı",
         "bulkAdjustedMixed": "{ok} ayarlandı, {skipped} atlandı",
-        "delDepleted": "{count} tükenmiş istemci silindi"
+        "delDepleted": "{count} tükenmiş kullanıcı silindi"
       }
     },
     "groups": {
       "title": "Gruplar",
       "name": "İsim",
-      "clientCount": "Gruptaki istemciler",
+      "clientCount": "Gruptaki kullanıcılar",
       "totalGroups": "Toplam grup",
-      "totalGroupedClients": "Grubu olan istemciler",
+      "totalGroupedClients": "Grubu olan kullanıcılar",
       "emptyGroups": "Boş gruplar",
       "addGroup": "Grup ekle",
       "createSuccess": "«{name}» grubu oluşturuldu.",
       "rename": "Yeniden adlandır",
       "renameTitle": "{name} yeniden adlandır",
       "renameCollision": "«{name}» adında bir grup zaten var.",
-      "renameSuccess": "{count} istemcinin grubu yeniden adlandırıldı.",
+      "renameSuccess": "{count} kullanıcının grubu yeniden adlandırıldı.",
       "deleteConfirmTitle": "{name} grubunu sil?",
-      "deleteConfirmContent": "Bu, grubu siler ve etiketini {count} istemciden temizler. İstemciler silinmez.",
-      "deleteSuccess": "{count} istemcinin grubu temizlendi.",
+      "deleteConfirmContent": "Bu, grubu siler ve etiketini {count} kullanıcıdan temizler. Kullanıcılar silinmez.",
+      "deleteSuccess": "{count} kullanıcının grubu temizlendi.",
       "resetTraffic": "Trafiği sıfırla",
       "resetConfirmTitle": "{name} grubunun trafiğini sıfırla?",
-      "resetConfirmContent": "Bu, bu gruptaki tüm {count} istemcinin yukarı/aşağı trafiğini sıfırlar.",
-      "resetSuccess": "{count} istemcinin trafiği sıfırlandı.",
-      "adjustSuccess": "{name} içinde {count} istemci ayarlandı.",
-      "emptyForAction": "Bu grupta henüz istemci yok.",
-      "deleteGroupOnly": "Grubu sil (istemcileri tut)",
-      "deleteClients": "Gruptaki istemcileri sil",
-      "deleteClientsConfirmTitle": "{name} içindeki tüm istemcileri sil?",
-      "deleteClientsConfirmContent": "Bu, {count} istemciyi trafik kayıtlarıyla birlikte kalıcı olarak siler. Grup etiketi de temizlenir. Geri alınamaz.",
-      "deleteClientsSuccess": "{count} istemci silindi.",
+      "resetConfirmContent": "Bu, bu gruptaki tüm {count} kullanıcının yukarı/aşağı trafiğini sıfırlar.",
+      "resetSuccess": "{count} kullanıcının trafiği sıfırlandı.",
+      "adjustSuccess": "{name} içinde {count} kullanıcı ayarlandı.",
+      "emptyForAction": "Bu grupta henüz kullanıcı yok.",
+      "deleteGroupOnly": "Grubu sil (kullanıcıları tut)",
+      "deleteClients": "Gruptaki kullanıcıları sil",
+      "deleteClientsConfirmTitle": "{name} içindeki tüm kullanıcıları sil?",
+      "deleteClientsConfirmContent": "Bu, {count} kullanıcıyı trafik kayıtlarıyla birlikte kalıcı olarak siler. Grup etiketi de temizlenir. Geri alınamaz.",
+      "deleteClientsSuccess": "{count} kullanıcı silindi.",
       "deleteClientsMixed": "{ok} silindi, {failed} atlandı",
-      "addToGroup": "İstemci ekle…",
-      "addToGroupTitle": "«{name}» grubuna istemci ekle",
-      "addToGroupDesc": "Bu gruba eklemek için istemcileri seçin. Mevcut gelen bağlantıları korunur; yalnızca grup etiketi değişir. Halihazırda bu grupta olan istemciler listelenmez.",
-      "addToGroupEmpty": "Eklenecek başka istemci yok.",
-      "addToGroupResult": "{count} istemci {name} grubuna eklendi.",
-      "removeFromGroup": "İstemci çıkar…",
-      "removeFromGroupTitle": "«{name}» grubundan istemci çıkar",
-      "removeFromGroupDesc": "Bu gruptan çıkarılacak üyeleri seçin. İstemciler korunur (tamamen kaldırmak için «Gruptaki istemcileri sil» kullanın).",
-      "removeFromGroupResult": "{name} grubundan {count} istemci çıkarıldı."
+      "addToGroup": "Kullanıcı ekle…",
+      "addToGroupTitle": "«{name}» grubuna kullanıcı ekle",
+      "addToGroupDesc": "Bu gruba eklemek için kullanıcıları seçin. Mevcut gelen bağlantıları korunur; yalnızca grup etiketi değişir. Halihazırda bu grupta olan kullanıcılar listelenmez.",
+      "addToGroupEmpty": "Eklenecek başka kullanıcı yok.",
+      "addToGroupResult": "{count} kullanıcı {name} grubuna eklendi.",
+      "removeFromGroup": "Kullanıcı çıkar…",
+      "removeFromGroupTitle": "«{name}» grubundan kullanıcı çıkar",
+      "removeFromGroupDesc": "Bu gruptan çıkarılacak üyeleri seçin. Kullanıcılar korunur (tamamen kaldırmak için «Gruptaki kullanıcıları sil» kullanın).",
+      "removeFromGroupResult": "{name} grubundan {count} kullanıcı çıkarıldı."
     },
     "nodes": {
       "title": "Düğümler",
@@ -954,9 +957,9 @@
       "panelUrlPath": "URI yolu",
       "panelUrlPathDesc": "Web paneli için URI yolu. ('/' ile başlar ve '/' ile biter)",
       "pageSize": "Sayfa Boyutu",
-      "pageSizeDesc": "Gelenler tablosu için sayfa boyutunu belirleyin. (0 = devre dışı)",
+      "pageSizeDesc": "Bağlantı Noktaları tablosu için sayfa boyutunu belirleyin. (0 = devre dışı)",
       "panelProxy": "Panel ağ proxy'si",
-      "panelProxyDesc": "Panelin kendi giden istekleri (geo güncellemeleri, Xray/panel sürüm kontrolleri, Telegram) bu proxy üzerinden yönlendirir; sunucu tarafındaki GitHub/Telegram filtrelemesini atlatmak için. socks5:// veya http(s):// kabul eder, örn. yerel bir Xray SOCKS geleni. Doğrudan bağlantı için boş bırakın.",
+      "panelProxyDesc": "Panelin kendi giden istekleri (geo güncellemeleri, Xray/panel sürüm kontrolleri, Telegram) bu proxy üzerinden yönlendirir; sunucu tarafındaki GitHub/Telegram filtrelemesini atlatmak için. socks5:// veya http(s):// kabul eder, örn. yerel bir Xray SOCKS bağlantı noktası. Doğrudan bağlantı için boş bırakın.",
       "remarkModel": "Açıklama Modeli & Ayırma Karakteri",
       "datepicker": "Takvim Türü",
       "datepickerPlaceholder": "Tarih Seçin",
@@ -1027,13 +1030,13 @@
       "subDomain": "Dinleme Alan Adı",
       "subDomainDesc": "Abonelik hizmeti için alan adı. (tüm alan adlarını ve IP'leri dinlemek için boş bırakın)",
       "subUpdates": "Güncelleme Aralıkları",
-      "subUpdatesDesc": "Müşteri uygulamalarındaki abonelik URL'sinin güncelleme aralıkları. (birim: saat)",
+      "subUpdatesDesc": "Kullanıcı uygulamalarındaki abonelik URL'sinin güncelleme aralıkları. (birim: saat)",
       "subEncrypt": "Kodla",
       "subEncryptDesc": "Abonelik hizmetinin döndürülen içeriği Base64 ile şifrelenir.",
       "subShowInfo": "Kullanım Bilgisini Göster",
-      "subShowInfoDesc": "Kalan trafik ve tarih müşteri uygulamalarında görüntülenir.",
+      "subShowInfoDesc": "Kalan trafik ve tarih istemci uygulamalarında görüntülenir.",
       "subEmailInRemark": "Ada Email Ekle",
-      "subEmailInRemarkDesc": "Abonelik profil adına istemcinin e-postasını dahil edin.",
+      "subEmailInRemarkDesc": "Abonelik profil adına kullanıcının e-postasını dahil edin.",
       "subURI": "Ters Proxy URI",
       "subURIDesc": "Proxy arkasında kullanılacak abonelik URL'sinin URI yolu.",
       "externalTrafficInformEnable": "Harici Trafik Bilgisi",
@@ -1041,7 +1044,7 @@
       "externalTrafficInformURI": "Harici Trafik Bilgisi URI'si",
       "externalTrafficInformURIDesc": "Trafik güncellemeleri bu URI'ye gönderildi.",
       "restartXrayOnClientDisable": "Otomatik Devre Dışı Sonrası Xray'i Yeniden Başlat",
-      "restartXrayOnClientDisableDesc": "Bir istemci süre dolumu veya trafik limiti nedeniyle otomatik devre dışı bırakıldığında Xray'i yeniden başlat.",
+      "restartXrayOnClientDisableDesc": "Bir kullanıcı süre dolumu veya trafik limiti nedeniyle otomatik devre dışı bırakıldığında Xray'i yeniden başlat.",
       "fragment": "Parçalama",
       "fragmentDesc": "TLS merhaba paketinin parçalanmasını etkinleştir.",
       "fragmentSett": "Parçalama Ayarları",
@@ -1070,11 +1073,11 @@
         "invertFlagDesc": "Öznitelik «devre dışı» anlamına geldiğinde etkinleştirin (örn. shadowInactive).",
         "syncSchedule": "Senkronizasyon programı",
         "syncScheduleDesc": "cron benzeri dize, örn. @every 1m",
-        "inboundTags": "Gelen etiketleri",
-        "inboundTagsDesc": "LDAP senkronizasyonunun istemci otomatik oluşturup/silebileceği gelenler.",
-        "noInbounds": "Gelen bulunamadı. Önce Gelenler'de bir tane oluşturun.",
-        "autoCreate": "İstemcileri otomatik oluştur",
-        "autoDelete": "İstemcileri otomatik sil",
+        "inboundTags": "Bağlantı noktası etiketleri",
+        "inboundTagsDesc": "LDAP senkronizasyonunun istemci otomatik oluşturup/silebileceği bağlantı noktaları.",
+        "noInbounds": "Bağlantı noktası bulunamadı. Önce Bağlantı Noktaları'nda bir tane oluşturun.",
+        "autoCreate": "Kullanıcıları otomatik oluştur",
+        "autoDelete": "Kullanıcıları otomatik sil",
         "defaultTotalGb": "Varsayılan toplam (GB)",
         "defaultExpiryDays": "Varsayılan son kullanma (gün)",
         "defaultIpLimit": "Varsayılan IP limiti"
@@ -1134,7 +1137,7 @@
         "apiTokenNamePlaceholder": "örn. central-panel-a",
         "apiTokenNameRequired": "Ad zorunludur",
         "apiTokenEmpty": "Henüz token yok — bot veya uzak panelleri doğrulamak için bir tane oluşturun.",
-        "apiTokenDeleteWarning": "Bu tokenı kullanan tüm istemciler anında kimlik doğrulamasını kaybeder.",
+        "apiTokenDeleteWarning": "Bu tokenı kullanan tüm kullanıcılar anında kimlik doğrulamasını kaybeder.",
         "apiTokenCreatedTitle": "Belirteç oluşturuldu",
         "apiTokenCreatedNotice": "Bu belirteci şimdi kopyalayın. Güvenlik nedeniyle okunabilir biçimde saklanmaz ve tekrar gösterilmez."
       },
@@ -1192,12 +1195,12 @@
       "outboundTestUrl": "Outbound test URL",
       "outboundTestUrlDesc": "Outbound bağlantı testinde kullanılan URL",
       "Torrent": "BitTorrent Protokolünü Engelle",
-      "Inbounds": "Gelenler",
-      "InboundsDesc": "Belirli müşterileri kabul eder.",
-      "Outbounds": "Gidenler",
+      "Inbounds": "Bağlantı Noktaları",
+      "InboundsDesc": "Belirli kullanıcıları kabul eder.",
+      "Outbounds": "Çıkış Noktaları",
       "Balancers": "Dengeler",
       "balancerTagRequired": "Etiket gereklidir",
-      "balancerSelectorRequired": "En az bir giden seçin",
+      "balancerSelectorRequired": "En az bir çıkış noktası seçin",
       "OutboundsDesc": "Giden trafiğin yolunu ayarlayın.",
       "Routings": "Yönlendirme Kuralları",
       "RoutingsDesc": "Her kuralın önceliği önemlidir!",
@@ -1236,8 +1239,8 @@
         "down": "Aşağı",
         "source": "Kaynak",
         "dest": "Hedef",
-        "inbound": "Gelen",
-        "outbound": "Giden",
+        "inbound": "Bağlantı Noktası",
+        "outbound": "Çıkış Noktası",
         "balancer": "Dengeler",
         "info": "Bilgi",
         "add": "Kural Ekle",
@@ -1254,25 +1257,25 @@
         "attributes": "Öznitelikler",
         "value": "Değer",
         "user": "Kullanıcı",
-        "inboundTags": "Gelen etiketleri",
-        "outboundTag": "Giden etiketi",
+        "inboundTags": "Bağlantı noktası etiketleri",
+        "outboundTag": "Çıkış noktası etiketi",
         "balancerTag": "Dengeleyici etiketi",
         "balancerTagTooltip": "Trafiği yapılandırılmış yük dengeleyicilerden biri üzerinden yönlendirir"
       },
       "outboundForm": {
-        "tagDuplicate": "Etiket başka bir giden tarafından kullanılıyor",
+        "tagDuplicate": "Etiket başka bir çıkış noktası tarafından kullanılıyor",
         "tagRequired": "Etiket gereklidir",
         "tagPlaceholder": "benzersiz-etiket",
         "localIpPlaceholder": "yerel IP",
-        "dialerProxyPlaceholder": "Zincirlemek için bir giden seçin",
-        "dialerProxyHint": "Bir proxy zinciri oluşturmak için bu gideni başka bir giden üzerinden (etikete göre) bağlayın. Doğrudan bağlanmak için boş bırakın.",
+        "dialerProxyPlaceholder": "Zincirlemek için bir çıkış noktası seçin",
+        "dialerProxyHint": "Bir proxy zinciri oluşturmak için bu çıkış noktasını başka bir çıkış noktası üzerinden (etikete göre) bağlayın. Doğrudan bağlanmak için boş bırakın.",
         "addressRequired": "Adres gereklidir",
         "portRequired": "Port gereklidir",
         "optional": "opsiyonel",
         "udpOverTcp": "UDP over TCP",
         "uotVersion": "UoT sürümü",
-        "inboundTag": "Gelen etiketi",
-        "inboundTagPlaceholder": "yönlendirme kurallarında kullanılan gelen etiketi",
+        "inboundTag": "Bağlantı noktası etiketi",
+        "inboundTagPlaceholder": "yönlendirme kurallarında kullanılan bağlantı noktası etiketi",
         "responseType": "Yanıt tipi",
         "rewriteNetwork": "Ağı yeniden yaz",
         "unchanged": "(değişmedi)",
@@ -1315,9 +1318,9 @@
         "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
       },
       "outbound": {
-        "addOutbound": "Giden Ekle",
+        "addOutbound": "Çıkış Noktası Ekle",
         "addReverse": "Ters Ekle",
-        "editOutbound": "Gideni Düzenle",
+        "editOutbound": "Çıkış Noktasını Düzenle",
         "editReverse": "Tersi Düzenle",
         "reverseTag": "Ters Etiket",
         "reverseTagDesc": "VLESS basit ters proxy çıkış etiketi. Devre dışı bırakmak için boş bırakın.",
@@ -1334,14 +1337,14 @@
         "intercon": "Bağlantı",
         "settings": "Ayarlar",
         "accountInfo": "Hesap Bilgileri",
-        "outboundStatus": "Giden Durumu",
+        "outboundStatus": "Çıkış Noktası Durumu",
         "sendThrough": "Üzerinden Gönder",
         "test": "Test",
         "testResult": "Test Sonucu",
         "testing": "Bağlantı test ediliyor...",
         "testSuccess": "Test başarılı",
         "testFailed": "Test başarısız",
-        "testError": "Giden test edilemedi",
+        "testError": "Çıkış noktası test edilemedi",
         "testModeTooltip": "TCP: hızlı dial-only probe. HTTP: xray üzerinden tam istek.",
         "testAll": "Tümünü test et",
         "nordvpn": "NordVPN",
@@ -1370,7 +1373,7 @@
         "tolerance": "Tolerans",
         "baselines": "Baselines",
         "costs": "Costs",
-        "balancerDesc": "Dengeleyici Etiketi ve Giden Etiketi aynı anda kullanılamaz. Aynı anda kullanıldığında yalnızca giden etiketi çalışır."
+        "balancerDesc": "Dengeleyici Etiketi ve Giden Etiketi aynı anda kullanılamaz. Aynı anda kullanıldığında yalnızca çıkış noktası etiketi çalışır."
       },
       "wireguard": {
         "secretKey": "Gizli Anahtar",
@@ -1391,8 +1394,8 @@
         "privateKey": "Özel anahtar",
         "noServers": "Seçilen ülke için sunucu bulunamadı",
         "noPublicKey": "Seçilen sunucu NordLynx genel anahtarı yayınlamıyor.",
-        "outboundAdded": "NordVPN giden eklendi",
-        "outboundUpdated": "NordVPN giden güncellendi"
+        "outboundAdded": "NordVPN çıkış noktası eklendi",
+        "outboundUpdated": "NordVPN çıkış noktası güncellendi"
       },
       "warp": {
         "licenseError": "WARP lisansı ayarlanamadı.",
@@ -1416,13 +1419,13 @@
         "warpPlusData": "WARP+ veri",
         "quota": "Kota",
         "usage": "Kullanım",
-        "addOutbound": "Giden ekle"
+        "addOutbound": "Çıkış noktası ekle"
       },
       "dns": {
         "enable": "DNS'yi Etkinleştir",
         "enableDesc": "Dahili DNS sunucusunu etkinleştir",
-        "tag": "DNS Gelen Etiketi",
-        "tagDesc": "Bu etiket, yönlendirme kurallarında Gelen etiketi olarak kullanılabilir.",
+        "tag": "DNS Bağlantı Noktası Etiketi",
+        "tagDesc": "Bu etiket, yönlendirme kurallarında Bağlantı noktası etiketi olarak kullanılabilir.",
         "clientIp": "İstemci IP",
         "clientIpDesc": "DNS sorguları sırasında belirtilen IP konumunu sunucuya bildirmek için kullanılır",
         "disableCache": "Önbelleği devre dışı bırak",
@@ -1475,8 +1478,8 @@
     "noQuery": "❌ Sorgu bulunamadı! Lütfen komutu tekrar kullanın!",
     "wentWrong": "❌ Bir şeyler yanlış gitti!",
     "noIpRecord": "❗ IP Kaydı Yok!",
-    "noInbounds": "❗ Gelen bağlantı bulunamadı!",
-    "unlimited": "♾ Sınırsız (Sıfırla)",
+    "noInbounds": "❗ Bağlantı noktası bulunamadı!",
+    "unlimited": "♾ Sınırsız",
     "add": "Ekle",
     "month": "Ay",
     "months": "Aylar",
@@ -1485,8 +1488,8 @@
     "hours": "Saatler",
     "minutes": "Dakika",
     "unknown": "Bilinmeyen",
-    "inbounds": "Gelenler",
-    "clients": "İstemciler",
+    "inbounds": "Bağlantı Noktaları",
+    "clients": "Kullanıcılar",
     "offline": "🔴 Çevrimdışı",
     "online": "🟢 Çevrimiçi",
     "commands": {
@@ -1498,8 +1501,8 @@
       "status": "✅ Bot çalışıyor!",
       "usage": "❗ Lütfen aramak için bir metin sağlayın!",
       "getID": "🆔 Kimliğiniz: <code>{{ .ID }}</code>",
-      "helpAdminCommands": "Xray Core'u yeniden başlatmak için:\r\n<code>/restart</code>\r\n\r\nBir müşteri e-postasını aramak için:\r\n<code>/usage [E-posta]</code>\r\n\r\nGelenleri aramak için (müşteri istatistikleri ile):\r\n<code>/inbound [Açıklama]</code>\r\n\r\nTelegram Sohbet Kimliği:\r\n<code>/id</code>",
-      "helpClientCommands": "İstatistikleri aramak için şu komutu kullanın:\r\n\r\n<code>/usage [E-posta]</code>\r\n\r\nTelegram Sohbet Kimliği:\r\n<code>/id</code>",
+      "helpAdminCommands": "Xray Core'u yeniden başlatmak için:\r\n<code>/restart</code>\r\n\r\nBir kullanıcının istatistiklerini aramak için:\r\n<code>/usage [E-posta]</code>\r\n\r\nBağlantı noktalarını aramak için (kullanıcı istatistikleri ile):\r\n<code>/inbound [Açıklama]</code>\r\n\r\nTelegram Sohbet Kimliği (Chat ID):\r\n<code>/id</code>",
+      "helpClientCommands": "İstatistiklerinizi görmek için şu komutu kullanın:\r\n\r\n<code>/usage [E-posta]</code>\r\n\r\nTelegram Sohbet Kimliği:\r\n<code>/id</code>",
       "restartUsage": "\r\n\r\n<code>/restart</code>",
       "restartSuccess": "✅ İşlem başarılı!",
       "restartFailed": "❗ İşlem hatası.\r\n\r\n<code>Hata: {{ .Error }}</code>.",
@@ -1535,46 +1538,46 @@
       "username": "👤 Kullanıcı Adı: {{ .Username }}\r\n",
       "reason": "❗️ Sebep: {{ .Reason }}\r\n",
       "time": "⏰ Zaman: {{ .Time }}\r\n",
-      "inbound": "📍 Gelen: {{ .Remark }}\r\n",
+      "inbound": "📍 Bağlantı Noktası: {{ .Remark }}\r\n",
       "port": "🔌 Port: {{ .Port }}\r\n",
       "expire": "📅 Son Kullanma Tarihi: {{ .Time }}\r\n",
       "expireIn": "📅 Sona Erecek: {{ .Time }}\r\n",
       "active": "💡 Aktif: {{ .Enable }}\r\n",
       "enabled": "🚨 Etkin: {{ .Enable }}\r\n",
       "online": "🌐 Bağlantı durumu: {{ .Status }}\r\n",
-      "lastOnline": "🔙 Son çevrimiçi: {{ .Time }}\r\n",
+      "lastOnline": "🔙 Son Çevrimiçi: {{ .Time }}\r\n",
       "email": "📧 Email: {{ .Email }}\r\n",
       "upload": "🔼 Yükleme: ↑{{ .Upload }}\r\n",
       "download": "🔽 İndirme: ↓{{ .Download }}\r\n",
       "total": "📊 Toplam: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
       "TGUser": "👤 Telegram Kullanıcısı: {{ .TelegramID }}\r\n",
-      "exhaustedMsg": "🚨 Tükenmiş {{ .Type }}:\r\n",
-      "exhaustedCount": "🚨 Tükenmiş {{ .Type }} sayısı:\r\n",
-      "onlinesCount": "🌐 Çevrimiçi Müşteriler: {{ .Count }}\r\n",
+      "exhaustedMsg": "🚨 Limiti Dolan {{ .Type }}:\r\n",
+      "exhaustedCount": "🚨 Limiti Dolan {{ .Type }} sayısı:\r\n",
+      "onlinesCount": "🌐 Çevrimiçi Kullanıcılar: {{ .Count }}\r\n",
       "disabled": "🛑 Devre Dışı: {{ .Disabled }}\r\n",
-      "depleteSoon": "🔜 Yakında Tükenecek: {{ .Deplete }}\r\n\r\n",
+      "depleteSoon": "🔜 Kotası Dolmak Üzere: {{ .Deplete }}\r\n\r\n",
       "backupTime": "🗄 Yedekleme Zamanı: {{ .Time }}\r\n",
       "refreshedOn": "\r\n📋🔄 Yenilendi: {{ .Time }}\r\n\r\n",
       "yes": "✅ Evet",
       "no": "❌ Hayır",
-      "received_id": "🔑📥 Kimlik güncellendi.",
+      "received_id": "🔑📥 UUID güncellendi.",
       "received_password": "🔑📥 Şifre güncellendi.",
       "received_email": "📧📥 E-posta güncellendi.",
       "received_comment": "💬📥 Yorum güncellendi.",
-      "id_prompt": "🔑 Varsayılan Kimlik: {{ .ClientId }}\n\nKimliğinizi girin.",
+      "id_prompt": "🔑 Mevcut UUID: {{ .ClientId }}\n\nYeni UUID'nizi girin.",
       "pass_prompt": "🔑 Varsayılan Şifre: {{ .ClientPassword }}\n\nŞifrenizi girin.",
       "email_prompt": "📧 Varsayılan E-posta: {{ .ClientEmail }}\n\nE-postanızı girin.",
       "comment_prompt": "💬 Varsayılan Yorum: {{ .ClientComment }}\n\nYorumunuzu girin.",
-      "inbound_client_data_id": "🔄 Giriş: {{ .InboundRemark }}\n\n🔑 Kimlik: {{ .ClientId }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Trafik: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Yorum: {{ .ClientComment }}\n\nArtık bu müşteriyi girişe ekleyebilirsin!",
-      "inbound_client_data_pass": "🔄 Giriş: {{ .InboundRemark }}\n\n🔑 Şifre: {{ .ClientPass }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Trafik: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Yorum: {{ .ClientComment }}\n\nArtık bu müşteriyi girişe ekleyebilirsin!",
+      "inbound_client_data_id": "🔄 Bağlantı Noktası: {{ .InboundRemark }}\n\n🔑 UUID: {{ .ClientId }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Kota: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Açıklama: {{ .ClientComment }}\n\nArtık bu kullanıcıyı bağlantı noktasına ekleyebilirsiniz!",
+      "inbound_client_data_pass": "🔄 Bağlantı Noktası: {{ .InboundRemark }}\n\n🔑 Şifre: {{ .ClientPass }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Kota: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Açıklama: {{ .ClientComment }}\n\nArtık bu kullanıcıyı bağlantı noktasına ekleyebilirsiniz!",
       "cancel": "❌ İşlem iptal edildi! \n\nİstediğiniz zaman /start ile yeniden başlayabilirsiniz. 🔄",
       "error_add_client": "⚠️ Hata:\n\n {{ .error }}",
       "using_default_value": "Tamam, varsayılan değeri kullanacağım. 😊",
       "incorrect_input": "Girdiğiniz değer geçerli değil.\nKelime öbekleri boşluk olmadan devam etmelidir.\nDoğru örnek: aaaaaa\nYanlış örnek: aaa aaa 🚫",
-      "AreYouSure": "Emin misin? 🤔",
+      "AreYouSure": "Emin misiniz? 🤔",
       "SuccessResetTraffic": "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ✅ Başarılı",
       "FailedResetTraffic": "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ❌ Başarısız \n\n🛠️ Hata: [ {{ .ErrorMessage }} ]",
-      "FinishProcess": "🔚 Tüm müşteriler için trafik sıfırlama işlemi tamamlandı."
+      "FinishProcess": "🔚 Tüm kullanıcılar için trafik sıfırlama işlemi tamamlandı."
     },
     "buttons": {
       "closeKeyboard": "❌ Klavyeyi Kapat",
@@ -1587,10 +1590,10 @@
       "confirmToggle": "✅ Kullanıcıyı Etkinleştirme/Devre Dışı Bırakmayı Onayla?",
       "dbBackup": "Veritabanı Yedeği Al",
       "serverUsage": "Sunucu Kullanımı",
-      "getInbounds": "Gelenleri Al",
-      "depleteSoon": "Yakında Tükenecek",
-      "clientUsage": "Kullanımı Al",
-      "onlines": "Çevrimiçi Müşteriler",
+      "getInbounds": "Bağlantı Noktalarını Al",
+      "depleteSoon": "Kotası Dolmak Üzere",
+      "clientUsage": "Kullanıcı İstatistikleri",
+      "onlines": "Çevrimiçi Kullanıcılar",
       "commands": "Komutlar",
       "refresh": "🔄 Yenile",
       "clearIPs": "❌ IP'leri Temizle",
@@ -1608,8 +1611,8 @@
       "confirmNumberAdd": "✅ Ekleme onayı: {{ .Num }}",
       "limitTraffic": "🚧 Trafik Sınırı",
       "getBanLogs": "Yasak Günlüklerini Al",
-      "allClients": "Tüm Müşteriler",
-      "addClient": "Müşteri Ekle",
+      "allClients": "Tüm Kullanıcılar",
+      "addClient": "Kullanıcı Ekle",
       "submitDisable": "Devre Dışı Olarak Gönder ☑️",
       "submitEnable": "Etkin Olarak Gönder ✅",
       "use_default": "🏷️ Varsayılanı Kullan",
@@ -1624,12 +1627,12 @@
     "answers": {
       "successfulOperation": "✅ İşlem başarılı!",
       "errorOperation": "❗ İşlemde hata.",
-      "getInboundsFailed": "❌ Gelenler alınamadı.",
-      "getClientsFailed": "❌ Müşteriler alınamadı.",
+      "getInboundsFailed": "❌ Bağlantı Noktaları alınamadı.",
+      "getClientsFailed": "❌ Kullanıcılar alınamadı.",
       "canceled": "❌ {{ .Email }}: İşlem iptal edildi.",
-      "clientRefreshSuccess": "✅ {{ .Email }}: Müşteri başarıyla yenilendi.",
+      "clientRefreshSuccess": "✅ {{ .Email }}: Kullanıcı başarıyla yenilendi.",
       "IpRefreshSuccess": "✅ {{ .Email }}: IP'ler başarıyla yenilendi.",
-      "TGIdRefreshSuccess": "✅ {{ .Email }}: Müşterinin Telegram Kullanıcısı başarıyla yenilendi.",
+      "TGIdRefreshSuccess": "✅ {{ .Email }}: Kullanıcının Telegram Kullanıcısı başarıyla yenilendi.",
       "resetTrafficSuccess": "✅ {{ .Email }}: Trafik başarıyla sıfırlandı.",
       "setTrafficLimitSuccess": "✅ {{ .Email }}: Trafik limiti başarıyla kaydedildi.",
       "expireResetSuccess": "✅ {{ .Email }}: Son kullanma günleri başarıyla sıfırlandı.",
@@ -1641,8 +1644,8 @@
       "enableSuccess": "✅ {{ .Email }}: Başarıyla etkinleştirildi.",
       "disableSuccess": "✅ {{ .Email }}: Başarıyla devre dışı bırakıldı.",
       "askToAddUserId": "Yapılandırmanız bulunamadı!\r\nLütfen yöneticinizden yapılandırmalarınıza Telegram ChatID'nizi eklemesini isteyin.\r\n\r\nKullanıcı ChatID'niz: <code>{{ .TgUserID }}</code>",
-      "chooseClient": "Gelen {{ .Inbound }} için bir Müşteri Seçin",
-      "chooseInbound": "Bir Gelen Seçin"
+      "chooseClient": "Bağlantı Noktası {{ .Inbound }} için bir Kullanıcı Seçin",
+      "chooseInbound": "Bir Bağlantı Noktası Seçin"
     }
   }
 }

+ 3 - 0
web/translation/uk-UA.json

@@ -501,6 +501,9 @@
         "accounts": "Акаунти",
         "allowTransparent": "Дозволити прозорий",
         "encryptionMethod": "Метод шифрування",
+        "fakeTlsDomain": "Домен FakeTLS (SNI)",
+        "mtprotoSecret": "Секрет",
+        "mtprotoHint": "MTProto обслуговується окремим процесом mtg, а не Xray. Налаштування транспорту та клієнти тут не застосовуються — поділіться посиланням нижче в Telegram.",
         "visionTestseed": "Vision testseed",
         "version": "Версія",
         "udpIdleTimeout": "UDP idle timeout (с)",

+ 3 - 0
web/translation/vi-VN.json

@@ -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)",

+ 3 - 0
web/translation/zh-CN.json

@@ -501,6 +501,9 @@
         "accounts": "账户",
         "allowTransparent": "允许透明",
         "encryptionMethod": "加密方法",
+        "fakeTlsDomain": "FakeTLS 域名 (SNI)",
+        "mtprotoSecret": "密钥",
+        "mtprotoHint": "MTProto 由独立的 mtg 进程提供服务,而非 Xray。传输设置和客户端在此不适用——请将下方链接分享到 Telegram。",
         "visionTestseed": "Vision testseed",
         "version": "版本",
         "udpIdleTimeout": "UDP 空闲超时 (s)",

+ 3 - 0
web/translation/zh-TW.json

@@ -501,6 +501,9 @@
         "accounts": "帳號",
         "allowTransparent": "允許透明",
         "encryptionMethod": "加密方法",
+        "fakeTlsDomain": "FakeTLS 網域 (SNI)",
+        "mtprotoSecret": "金鑰",
+        "mtprotoHint": "MTProto 由獨立的 mtg 程序提供服務,而非 Xray。傳輸設定與用戶端在此不適用——請將下方連結分享至 Telegram。",
         "visionTestseed": "Vision testseed",
         "version": "版本",
         "udpIdleTimeout": "UDP 閒置逾時 (s)",

+ 7 - 0
web/web.go

@@ -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()

+ 5 - 1
xray/process.go

@@ -23,7 +23,11 @@ import (
 
 // GetBinaryName returns the Xray binary filename for the current OS and architecture.
 func GetBinaryName() string {
-	return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH)
+	arch := runtime.GOARCH
+	if arch == "arm" {
+		arch = "arm32"
+	}
+	return fmt.Sprintf("xray-%s-%s", runtime.GOOS, arch)
 }
 
 // GetBinaryPath returns the full path to the Xray binary executable.