manager.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. package mtproto
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "net"
  8. "net/http"
  9. "os"
  10. "slices"
  11. "strconv"
  12. "strings"
  13. "sync"
  14. "time"
  15. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  16. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  17. )
  18. // SecretEntry is one named FakeTLS secret served by an mtg-multi process. Name is
  19. // the client email, used both as the [secrets] key and as the per-user key in the
  20. // /stats API so traffic can be attributed back to the client.
  21. type SecretEntry struct {
  22. Name string
  23. Secret string
  24. }
  25. // Instance is the desired runtime configuration of one mtproto inbound. A single
  26. // mtg-multi process serves every active client's secret through the [secrets]
  27. // section, so one inbound maps to one process with many named secrets.
  28. type Instance struct {
  29. Id int
  30. Tag string
  31. Listen string
  32. Port int
  33. Secrets []SecretEntry
  34. // Optional mtg tuning; each is omitted from the generated TOML when
  35. // zero-valued so mtg falls back to its own defaults.
  36. Debug bool
  37. ProxyProtocolListener bool
  38. PreferIP string
  39. FrontingIP string
  40. FrontingPort int
  41. FrontingProxyProtocol bool
  42. // ThrottleMaxConnections caps concurrent connections across all users with a
  43. // fair-share algorithm; zero disables throttling.
  44. ThrottleMaxConnections int
  45. // AdTag is a 32-hex Telegram advertising tag; when set, mtg routes clients
  46. // through Telegram middle proxies so a sponsored channel shows in their chat
  47. // list. It is part of the reloadable secret config, so a change is applied
  48. // via /reload without dropping connections. PublicIPv4/PublicIPv6 pin the
  49. // proxy's reachable address the middle proxy needs; they are omitted when
  50. // empty so mtg auto-detects, and a change forces a restart.
  51. AdTag string
  52. PublicIPv4 string
  53. PublicIPv6 string
  54. // When RouteThroughXray is set, mtg dials Telegram through the loopback
  55. // SOCKS bridge the panel injects into the Xray config at XrayRoutePort, so
  56. // the egress obeys the core's routing rules instead of going out directly.
  57. RouteThroughXray bool
  58. XrayRoutePort int
  59. }
  60. func (inst Instance) bindTo() string {
  61. listen := inst.Listen
  62. if listen == "" {
  63. listen = "0.0.0.0"
  64. }
  65. return fmt.Sprintf("%s:%d", listen, inst.Port)
  66. }
  67. // structuralFingerprint changes whenever a value outside the [secrets] section
  68. // of the generated TOML changes. Such a change can only be applied by
  69. // restarting mtg, unlike a secrets-only change, which a reload-capable mtg can
  70. // absorb in place.
  71. func (inst Instance) structuralFingerprint() string {
  72. parts := []string{
  73. inst.bindTo(),
  74. strconv.FormatBool(inst.Debug),
  75. strconv.FormatBool(inst.ProxyProtocolListener),
  76. inst.PreferIP,
  77. inst.FrontingIP,
  78. strconv.Itoa(inst.FrontingPort),
  79. strconv.FormatBool(inst.FrontingProxyProtocol),
  80. strconv.Itoa(inst.ThrottleMaxConnections),
  81. strconv.FormatBool(inst.RouteThroughXray),
  82. strconv.Itoa(inst.XrayRoutePort),
  83. inst.PublicIPv4,
  84. inst.PublicIPv6,
  85. }
  86. return strings.Join(parts, "|")
  87. }
  88. // secretsFingerprint identifies the reloadable secret config regardless of
  89. // client order, so a reordered clients array in the stored settings does not
  90. // read as a change. It moves whenever a client is added, removed, disabled, or
  91. // re-keyed, or the advertising tag changes — all of which mtg applies through
  92. // /reload without dropping connections.
  93. func (inst Instance) secretsFingerprint() string {
  94. pairs := make([]string, 0, len(inst.Secrets))
  95. for _, e := range inst.Secrets {
  96. pairs = append(pairs, e.Name+"="+e.Secret)
  97. }
  98. slices.Sort(pairs)
  99. return "adtag=" + inst.AdTag + "|" + strings.Join(pairs, "|")
  100. }
  101. // Traffic is a per-client traffic delta scraped from an mtg /stats endpoint. Tag
  102. // is the owning inbound's tag and Email is the client the bytes belong to.
  103. type Traffic struct {
  104. Tag string
  105. Email string
  106. Up int64
  107. Down int64
  108. }
  109. type clientCounters struct {
  110. up int64
  111. down int64
  112. }
  113. type managed struct {
  114. proc *Process
  115. tag string
  116. structuralFP string
  117. secretsFP string
  118. apiPort int
  119. last map[string]clientCounters
  120. }
  121. // Manager owns the set of running mtg processes keyed by inbound id.
  122. type Manager struct {
  123. mu sync.Mutex
  124. procs map[int]*managed
  125. // swept records that the one-time startup cleanup of orphaned mtg
  126. // processes (survivors of a previous x-ui run) has already run.
  127. swept bool
  128. }
  129. var (
  130. managerOnce sync.Once
  131. manager *Manager
  132. )
  133. // GetManager returns the process-wide mtg manager singleton.
  134. func GetManager() *Manager {
  135. managerOnce.Do(func() {
  136. manager = &Manager{procs: map[int]*managed{}}
  137. })
  138. return manager
  139. }
  140. // InstanceFromInbound derives a desired Instance from an mtproto inbound,
  141. // building one named secret per active client. Secrets are healed on save (see
  142. // normalizeMtprotoSecret) and by the migration, so they are read as-is here to
  143. // keep the fingerprint stable across reconciles. Returns false when the inbound
  144. // is not a usable mtproto inbound or has no active client secret to serve.
  145. func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
  146. if ib == nil || ib.Protocol != model.MTProto {
  147. return Instance{}, false
  148. }
  149. settings := ib.Settings
  150. var parsed struct {
  151. ProxyProtocolListener bool `json:"proxyProtocolListener"`
  152. Debug bool `json:"debug"`
  153. DomainFronting struct {
  154. IP string `json:"ip"`
  155. Port int `json:"port"`
  156. ProxyProtocol bool `json:"proxyProtocol"`
  157. } `json:"domainFronting"`
  158. PreferIP string `json:"preferIp"`
  159. ThrottleMaxConnections int `json:"throttleMaxConnections"`
  160. RouteThroughXray bool `json:"routeThroughXray"`
  161. RouteXrayPort int `json:"routeXrayPort"`
  162. AdTag string `json:"adTag"`
  163. PublicIPv4 string `json:"publicIpv4"`
  164. PublicIPv6 string `json:"publicIpv6"`
  165. Clients []struct {
  166. Email string `json:"email"`
  167. Secret string `json:"secret"`
  168. Enable bool `json:"enable"`
  169. } `json:"clients"`
  170. }
  171. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  172. return Instance{}, false
  173. }
  174. secrets := make([]SecretEntry, 0, len(parsed.Clients))
  175. for _, c := range parsed.Clients {
  176. if !c.Enable || c.Secret == "" || c.Email == "" {
  177. continue
  178. }
  179. secrets = append(secrets, SecretEntry{Name: c.Email, Secret: c.Secret})
  180. }
  181. if len(secrets) == 0 {
  182. return Instance{}, false
  183. }
  184. return Instance{
  185. Id: ib.Id,
  186. Tag: ib.Tag,
  187. Listen: ib.Listen,
  188. Port: ib.Port,
  189. Secrets: secrets,
  190. Debug: parsed.Debug,
  191. ProxyProtocolListener: parsed.ProxyProtocolListener,
  192. PreferIP: parsed.PreferIP,
  193. FrontingIP: parsed.DomainFronting.IP,
  194. FrontingPort: parsed.DomainFronting.Port,
  195. FrontingProxyProtocol: parsed.DomainFronting.ProxyProtocol,
  196. ThrottleMaxConnections: parsed.ThrottleMaxConnections,
  197. RouteThroughXray: parsed.RouteThroughXray,
  198. XrayRoutePort: parsed.RouteXrayPort,
  199. AdTag: strings.TrimSpace(parsed.AdTag),
  200. PublicIPv4: strings.TrimSpace(parsed.PublicIPv4),
  201. PublicIPv6: strings.TrimSpace(parsed.PublicIPv6),
  202. }, true
  203. }
  204. // Ensure starts the mtg process for an instance, or restarts it when its
  205. // configuration changed. A no-op when the desired process is already running.
  206. func (m *Manager) Ensure(inst Instance) error {
  207. m.mu.Lock()
  208. defer m.mu.Unlock()
  209. m.sweepOrphansLocked()
  210. return m.ensureLocked(inst)
  211. }
  212. // sweepOrphansLocked kills mtg processes left running by a previous x-ui run,
  213. // exactly once per process lifetime and before any of our own mtg are started.
  214. // Because x-ui owns every mtg process, anything alive at this point is an orphan
  215. // that would otherwise keep holding an inbound port with a stale secret.
  216. func (m *Manager) sweepOrphansLocked() {
  217. if m.swept {
  218. return
  219. }
  220. m.swept = true
  221. if n := killStrayMtgProcesses(GetBinaryPath()); n > 0 {
  222. logger.Warningf("mtproto: terminated %d orphaned mtg process(es) from a previous run", n)
  223. }
  224. }
  225. // ensureAction is what ensureLocked must do to move a running mtg process to a
  226. // desired instance: leave it alone, hot-reload only its secrets, or fully
  227. // restart it.
  228. type ensureAction int
  229. const (
  230. ensureNoop ensureAction = iota
  231. ensureReload
  232. ensureRestart
  233. )
  234. // ensureActionFor decides how to apply a desired instance to the currently
  235. // managed process. A structural change (or a dead process) forces a restart; a
  236. // secrets-only change is a candidate for an in-place reload; identical
  237. // fingerprints on a live process need nothing.
  238. func ensureActionFor(running bool, curStructFP, curSecretsFP, newStructFP, newSecretsFP string) ensureAction {
  239. if !running || curStructFP != newStructFP {
  240. return ensureRestart
  241. }
  242. if curSecretsFP != newSecretsFP {
  243. return ensureReload
  244. }
  245. return ensureNoop
  246. }
  247. func (m *Manager) ensureLocked(inst Instance) error {
  248. structFP := inst.structuralFingerprint()
  249. secFP := inst.secretsFingerprint()
  250. if cur, ok := m.procs[inst.Id]; ok {
  251. switch ensureActionFor(cur.proc.IsRunning(), cur.structuralFP, cur.secretsFP, structFP, secFP) {
  252. case ensureNoop:
  253. cur.tag = inst.Tag
  254. return nil
  255. case ensureReload:
  256. if err := writeConfig(configPathForID(inst.Id), inst, cur.apiPort); err != nil {
  257. return err
  258. }
  259. if applySecrets(cur.apiPort, inst) {
  260. cur.tag = inst.Tag
  261. cur.secretsFP = secFP
  262. logger.Infof("mtproto: applied secret update to inbound %d in place", inst.Id)
  263. return nil
  264. }
  265. logger.Warningf("mtproto: live secret update unavailable for inbound %d, restarting", inst.Id)
  266. fallthrough
  267. case ensureRestart:
  268. _ = cur.proc.Stop()
  269. delete(m.procs, inst.Id)
  270. }
  271. }
  272. apiPort, err := FreeLocalPort()
  273. if err != nil {
  274. return err
  275. }
  276. cfgPath := configPathForID(inst.Id)
  277. if err := writeConfig(cfgPath, inst, apiPort); err != nil {
  278. return err
  279. }
  280. proc := newProcess(cfgPath, fmt.Sprintf("inbound %d", inst.Id))
  281. if err := proc.Start(); err != nil {
  282. return err
  283. }
  284. m.procs[inst.Id] = &managed{
  285. proc: proc,
  286. tag: inst.Tag,
  287. structuralFP: structFP,
  288. secretsFP: secFP,
  289. apiPort: apiPort,
  290. last: map[string]clientCounters{},
  291. }
  292. logger.Infof("mtproto: started mtg for inbound %d on %s", inst.Id, inst.bindTo())
  293. return nil
  294. }
  295. // Remove stops and forgets the mtg process for an inbound id.
  296. func (m *Manager) Remove(id int) {
  297. m.mu.Lock()
  298. defer m.mu.Unlock()
  299. if cur, ok := m.procs[id]; ok {
  300. _ = cur.proc.Stop()
  301. delete(m.procs, id)
  302. _ = os.Remove(configPathForID(id))
  303. logger.Infof("mtproto: stopped mtg for inbound %d", id)
  304. }
  305. }
  306. // Reconcile drives the running set toward the desired instances: it stops
  307. // processes that are no longer wanted and (re)starts the rest. Used at boot
  308. // and periodically to recover from crashes.
  309. func (m *Manager) Reconcile(desired []Instance) {
  310. m.mu.Lock()
  311. defer m.mu.Unlock()
  312. m.sweepOrphansLocked()
  313. want := make(map[int]struct{}, len(desired))
  314. for _, inst := range desired {
  315. want[inst.Id] = struct{}{}
  316. }
  317. for id, cur := range m.procs {
  318. if _, ok := want[id]; !ok {
  319. _ = cur.proc.Stop()
  320. delete(m.procs, id)
  321. _ = os.Remove(configPathForID(id))
  322. }
  323. }
  324. for _, inst := range desired {
  325. if err := m.ensureLocked(inst); err != nil {
  326. logger.Warningf("mtproto: reconcile failed for inbound %d: %v", inst.Id, err)
  327. }
  328. }
  329. }
  330. // StopAll stops every managed mtg process. Called on panel shutdown.
  331. func (m *Manager) StopAll() {
  332. m.mu.Lock()
  333. defer m.mu.Unlock()
  334. for id, cur := range m.procs {
  335. _ = cur.proc.Stop()
  336. _ = os.Remove(configPathForID(id))
  337. delete(m.procs, id)
  338. }
  339. }
  340. // CollectTraffic scrapes each running mtg /stats endpoint and returns the
  341. // per-client byte deltas since the previous scrape, plus the emails of clients
  342. // with at least one live connection.
  343. func (m *Manager) CollectTraffic() ([]Traffic, []string) {
  344. type snap struct {
  345. id int
  346. apiPort int
  347. tag string
  348. last map[string]clientCounters
  349. }
  350. m.mu.Lock()
  351. snaps := make([]snap, 0, len(m.procs))
  352. for id, cur := range m.procs {
  353. if cur.proc == nil || !cur.proc.IsRunning() {
  354. continue
  355. }
  356. lastCopy := make(map[string]clientCounters, len(cur.last))
  357. for k, v := range cur.last {
  358. lastCopy[k] = v
  359. }
  360. snaps = append(snaps, snap{id: id, apiPort: cur.apiPort, tag: cur.tag, last: lastCopy})
  361. }
  362. m.mu.Unlock()
  363. var out []Traffic
  364. var online []string
  365. for _, s := range snaps {
  366. users, ok := scrapeStats(s.apiPort)
  367. if !ok {
  368. continue
  369. }
  370. newLast := make(map[string]clientCounters, len(users))
  371. for email, u := range users {
  372. up := u.BytesIn
  373. down := u.BytesOut
  374. newLast[email] = clientCounters{up: up, down: down}
  375. if u.Connections > 0 {
  376. online = append(online, email)
  377. }
  378. prev, had := s.last[email]
  379. if !had {
  380. continue
  381. }
  382. du := up - prev.up
  383. dd := down - prev.down
  384. if du < 0 {
  385. du = 0
  386. }
  387. if dd < 0 {
  388. dd = 0
  389. }
  390. if du > 0 || dd > 0 {
  391. out = append(out, Traffic{Tag: s.tag, Email: email, Up: du, Down: dd})
  392. }
  393. }
  394. m.mu.Lock()
  395. if cur, ok := m.procs[s.id]; ok {
  396. cur.last = newLast
  397. }
  398. m.mu.Unlock()
  399. }
  400. return out, online
  401. }
  402. // FreeLocalPort asks the OS for an unused loopback TCP port. It is used both
  403. // for mtg's /stats API endpoint and to allocate the per-inbound SOCKS egress
  404. // bridge port persisted into mtproto inbound settings.
  405. func FreeLocalPort() (int, error) {
  406. l, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", "127.0.0.1:0")
  407. if err != nil {
  408. return 0, err
  409. }
  410. defer l.Close()
  411. return l.Addr().(*net.TCPAddr).Port, nil
  412. }
  413. // renderConfig builds the mtg-multi TOML for an instance. Top-level keys must
  414. // precede any [section] header in TOML, and [secrets] must be the final section
  415. // so trailing keys are not swallowed by another table. The layout is therefore:
  416. // top-level scalars (incl. api-bind-to), then [domain-fronting], [network] and
  417. // [throttle], and finally [secrets] with one named secret per active client.
  418. func renderConfig(inst Instance, apiPort int) string {
  419. var b strings.Builder
  420. fmt.Fprintf(&b, "bind-to = %q\n", inst.bindTo())
  421. if inst.Debug {
  422. b.WriteString("debug = true\n")
  423. }
  424. if inst.ProxyProtocolListener {
  425. b.WriteString("proxy-protocol-listener = true\n")
  426. }
  427. if inst.PreferIP != "" {
  428. fmt.Fprintf(&b, "prefer-ip = %q\n", inst.PreferIP)
  429. }
  430. fmt.Fprintf(&b, "api-bind-to = \"127.0.0.1:%d\"\n", apiPort)
  431. if inst.AdTag != "" {
  432. fmt.Fprintf(&b, "ad-tag = %q\n", inst.AdTag)
  433. }
  434. if inst.PublicIPv4 != "" {
  435. fmt.Fprintf(&b, "public-ipv4 = %q\n", inst.PublicIPv4)
  436. }
  437. if inst.PublicIPv6 != "" {
  438. fmt.Fprintf(&b, "public-ipv6 = %q\n", inst.PublicIPv6)
  439. }
  440. if inst.FrontingIP != "" || inst.FrontingPort > 0 || inst.FrontingProxyProtocol {
  441. b.WriteString("\n[domain-fronting]\n")
  442. if inst.FrontingIP != "" {
  443. fmt.Fprintf(&b, "host = %q\n", inst.FrontingIP)
  444. }
  445. if inst.FrontingPort > 0 {
  446. fmt.Fprintf(&b, "port = %d\n", inst.FrontingPort)
  447. }
  448. if inst.FrontingProxyProtocol {
  449. b.WriteString("proxy-protocol = true\n")
  450. }
  451. }
  452. // When the inbound opts into Xray routing, mtg reaches Telegram through the
  453. // loopback SOCKS bridge the panel injects into the running Xray config. mtg
  454. // only supports SOCKS5 upstreams, which is exactly what the bridge exposes.
  455. if inst.RouteThroughXray && inst.XrayRoutePort > 0 {
  456. fmt.Fprintf(&b, "\n[network]\nproxies = [\"socks5://127.0.0.1:%d\"]\n", inst.XrayRoutePort)
  457. }
  458. if inst.ThrottleMaxConnections > 0 {
  459. fmt.Fprintf(&b, "\n[throttle]\nmax-connections = %d\n", inst.ThrottleMaxConnections)
  460. }
  461. b.WriteString("\n[secrets]\n")
  462. for _, e := range inst.Secrets {
  463. fmt.Fprintf(&b, "%q = %q\n", e.Name, e.Secret)
  464. }
  465. return b.String()
  466. }
  467. func writeConfig(path string, inst Instance, apiPort int) error {
  468. if err := os.MkdirAll(configDir(), 0o750); err != nil {
  469. return err
  470. }
  471. return os.WriteFile(path, []byte(renderConfig(inst, apiPort)), 0o640)
  472. }
  473. // statsUser is one entry of the mtg-multi /stats users map. bytes_in is traffic
  474. // the client sent to the proxy (upload) and bytes_out is what the proxy returned
  475. // (download).
  476. type statsUser struct {
  477. Connections int64 `json:"connections"`
  478. BytesIn int64 `json:"bytes_in"`
  479. BytesOut int64 `json:"bytes_out"`
  480. }
  481. type secretPutEntry struct {
  482. Secret string `json:"secret"`
  483. }
  484. type secretsPutBody struct {
  485. Secrets map[string]secretPutEntry `json:"secrets"`
  486. AdTag string `json:"ad_tag,omitempty"`
  487. }
  488. func secretsPayload(inst Instance) secretsPutBody {
  489. secrets := make(map[string]secretPutEntry, len(inst.Secrets))
  490. for _, e := range inst.Secrets {
  491. secrets[e.Name] = secretPutEntry{Secret: e.Secret}
  492. }
  493. return secretsPutBody{Secrets: secrets, AdTag: inst.AdTag}
  494. }
  495. // applySecrets pushes the desired secret set and advertising tag to a running
  496. // mtg-multi through its management API (PUT /secrets on the same loopback port
  497. // that serves /stats), so a client add, removal, re-key, or ad-tag change is
  498. // applied in place. mtg keeps every connection whose secret is unchanged and
  499. // closes only the removed or re-keyed ones. It returns true only on a 200: an
  500. // older binary without the endpoint (404), a refused connection, or any other
  501. // status yields false, so the caller falls back to a full restart.
  502. func applySecrets(port int, inst Instance) bool {
  503. body, err := json.Marshal(secretsPayload(inst))
  504. if err != nil {
  505. return false
  506. }
  507. client := http.Client{Timeout: 3 * time.Second}
  508. req, err := http.NewRequestWithContext(context.Background(), http.MethodPut, fmt.Sprintf("http://127.0.0.1:%d/secrets", port), bytes.NewReader(body))
  509. if err != nil {
  510. return false
  511. }
  512. req.Header.Set("Content-Type", "application/json")
  513. resp, err := client.Do(req)
  514. if err != nil {
  515. return false
  516. }
  517. defer resp.Body.Close()
  518. return resp.StatusCode == http.StatusOK
  519. }
  520. // scrapeStats reads the mtg-multi /stats JSON API and returns the per-user
  521. // cumulative counters. Best-effort: an unreachable endpoint or unparseable body
  522. // yields ok=false.
  523. func scrapeStats(port int) (map[string]statsUser, bool) {
  524. client := http.Client{Timeout: 3 * time.Second}
  525. req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf("http://127.0.0.1:%d/stats", port), nil)
  526. if err != nil {
  527. return nil, false
  528. }
  529. resp, err := client.Do(req)
  530. if err != nil {
  531. return nil, false
  532. }
  533. defer resp.Body.Close()
  534. var parsed struct {
  535. Users map[string]statsUser `json:"users"`
  536. }
  537. if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
  538. return nil, false
  539. }
  540. return parsed.Users, true
  541. }