1
0

manager.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. package mtproto
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "net"
  7. "net/http"
  8. "os"
  9. "strconv"
  10. "strings"
  11. "sync"
  12. "time"
  13. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  14. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  15. )
  16. // SecretEntry is one named FakeTLS secret served by an mtg-multi process. Name is
  17. // the client email, used both as the [secrets] key and as the per-user key in the
  18. // /stats API so traffic can be attributed back to the client.
  19. type SecretEntry struct {
  20. Name string
  21. Secret string
  22. }
  23. // Instance is the desired runtime configuration of one mtproto inbound. A single
  24. // mtg-multi process serves every active client's secret through the [secrets]
  25. // section, so one inbound maps to one process with many named secrets.
  26. type Instance struct {
  27. Id int
  28. Tag string
  29. Listen string
  30. Port int
  31. Secrets []SecretEntry
  32. // Optional mtg tuning; each is omitted from the generated TOML when
  33. // zero-valued so mtg falls back to its own defaults.
  34. Debug bool
  35. ProxyProtocolListener bool
  36. PreferIP string
  37. FrontingIP string
  38. FrontingPort int
  39. FrontingProxyProtocol bool
  40. // ThrottleMaxConnections caps concurrent connections across all users with a
  41. // fair-share algorithm; zero disables throttling.
  42. ThrottleMaxConnections int
  43. // When RouteThroughXray is set, mtg dials Telegram through the loopback
  44. // SOCKS bridge the panel injects into the Xray config at XrayRoutePort, so
  45. // the egress obeys the core's routing rules instead of going out directly.
  46. RouteThroughXray bool
  47. XrayRoutePort int
  48. }
  49. func (inst Instance) bindTo() string {
  50. listen := inst.Listen
  51. if listen == "" {
  52. listen = "0.0.0.0"
  53. }
  54. return fmt.Sprintf("%s:%d", listen, inst.Port)
  55. }
  56. // fingerprint changes whenever any value that ends up in the generated TOML
  57. // changes, so ensureLocked restarts mtg when the operator edits a setting or a
  58. // client is added, removed, disabled, or re-keyed.
  59. func (inst Instance) fingerprint() string {
  60. parts := []string{
  61. inst.bindTo(),
  62. strconv.FormatBool(inst.Debug),
  63. strconv.FormatBool(inst.ProxyProtocolListener),
  64. inst.PreferIP,
  65. inst.FrontingIP,
  66. strconv.Itoa(inst.FrontingPort),
  67. strconv.FormatBool(inst.FrontingProxyProtocol),
  68. strconv.Itoa(inst.ThrottleMaxConnections),
  69. strconv.FormatBool(inst.RouteThroughXray),
  70. strconv.Itoa(inst.XrayRoutePort),
  71. }
  72. for _, e := range inst.Secrets {
  73. parts = append(parts, e.Name+"="+e.Secret)
  74. }
  75. return strings.Join(parts, "|")
  76. }
  77. // Traffic is a per-client traffic delta scraped from an mtg /stats endpoint. Tag
  78. // is the owning inbound's tag and Email is the client the bytes belong to.
  79. type Traffic struct {
  80. Tag string
  81. Email string
  82. Up int64
  83. Down int64
  84. }
  85. type clientCounters struct {
  86. up int64
  87. down int64
  88. }
  89. type managed struct {
  90. proc *Process
  91. tag string
  92. fingerprint string
  93. apiPort int
  94. last map[string]clientCounters
  95. }
  96. // Manager owns the set of running mtg processes keyed by inbound id.
  97. type Manager struct {
  98. mu sync.Mutex
  99. procs map[int]*managed
  100. // swept records that the one-time startup cleanup of orphaned mtg
  101. // processes (survivors of a previous x-ui run) has already run.
  102. swept bool
  103. }
  104. var (
  105. managerOnce sync.Once
  106. manager *Manager
  107. )
  108. // GetManager returns the process-wide mtg manager singleton.
  109. func GetManager() *Manager {
  110. managerOnce.Do(func() {
  111. manager = &Manager{procs: map[int]*managed{}}
  112. })
  113. return manager
  114. }
  115. // InstanceFromInbound derives a desired Instance from an mtproto inbound,
  116. // building one named secret per active client. Secrets are healed on save (see
  117. // normalizeMtprotoSecret) and by the migration, so they are read as-is here to
  118. // keep the fingerprint stable across reconciles. Returns false when the inbound
  119. // is not a usable mtproto inbound or has no active client secret to serve.
  120. func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
  121. if ib == nil || ib.Protocol != model.MTProto {
  122. return Instance{}, false
  123. }
  124. settings := ib.Settings
  125. var parsed struct {
  126. ProxyProtocolListener bool `json:"proxyProtocolListener"`
  127. Debug bool `json:"debug"`
  128. DomainFronting struct {
  129. IP string `json:"ip"`
  130. Port int `json:"port"`
  131. ProxyProtocol bool `json:"proxyProtocol"`
  132. } `json:"domainFronting"`
  133. PreferIP string `json:"preferIp"`
  134. ThrottleMaxConnections int `json:"throttleMaxConnections"`
  135. RouteThroughXray bool `json:"routeThroughXray"`
  136. RouteXrayPort int `json:"routeXrayPort"`
  137. Clients []struct {
  138. Email string `json:"email"`
  139. Secret string `json:"secret"`
  140. Enable bool `json:"enable"`
  141. } `json:"clients"`
  142. }
  143. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  144. return Instance{}, false
  145. }
  146. secrets := make([]SecretEntry, 0, len(parsed.Clients))
  147. for _, c := range parsed.Clients {
  148. if !c.Enable || c.Secret == "" || c.Email == "" {
  149. continue
  150. }
  151. secrets = append(secrets, SecretEntry{Name: c.Email, Secret: c.Secret})
  152. }
  153. if len(secrets) == 0 {
  154. return Instance{}, false
  155. }
  156. return Instance{
  157. Id: ib.Id,
  158. Tag: ib.Tag,
  159. Listen: ib.Listen,
  160. Port: ib.Port,
  161. Secrets: secrets,
  162. Debug: parsed.Debug,
  163. ProxyProtocolListener: parsed.ProxyProtocolListener,
  164. PreferIP: parsed.PreferIP,
  165. FrontingIP: parsed.DomainFronting.IP,
  166. FrontingPort: parsed.DomainFronting.Port,
  167. FrontingProxyProtocol: parsed.DomainFronting.ProxyProtocol,
  168. ThrottleMaxConnections: parsed.ThrottleMaxConnections,
  169. RouteThroughXray: parsed.RouteThroughXray,
  170. XrayRoutePort: parsed.RouteXrayPort,
  171. }, true
  172. }
  173. // Ensure starts the mtg process for an instance, or restarts it when its
  174. // configuration changed. A no-op when the desired process is already running.
  175. func (m *Manager) Ensure(inst Instance) error {
  176. m.mu.Lock()
  177. defer m.mu.Unlock()
  178. m.sweepOrphansLocked()
  179. return m.ensureLocked(inst)
  180. }
  181. // sweepOrphansLocked kills mtg processes left running by a previous x-ui run,
  182. // exactly once per process lifetime and before any of our own mtg are started.
  183. // Because x-ui owns every mtg process, anything alive at this point is an orphan
  184. // that would otherwise keep holding an inbound port with a stale secret.
  185. func (m *Manager) sweepOrphansLocked() {
  186. if m.swept {
  187. return
  188. }
  189. m.swept = true
  190. if n := killStrayMtgProcesses(GetBinaryPath()); n > 0 {
  191. logger.Warningf("mtproto: terminated %d orphaned mtg process(es) from a previous run", n)
  192. }
  193. }
  194. func (m *Manager) ensureLocked(inst Instance) error {
  195. fp := inst.fingerprint()
  196. if cur, ok := m.procs[inst.Id]; ok {
  197. if cur.fingerprint == fp && cur.proc.IsRunning() {
  198. cur.tag = inst.Tag
  199. return nil
  200. }
  201. _ = cur.proc.Stop()
  202. delete(m.procs, inst.Id)
  203. }
  204. apiPort, err := FreeLocalPort()
  205. if err != nil {
  206. return err
  207. }
  208. cfgPath := configPathForID(inst.Id)
  209. if err := writeConfig(cfgPath, inst, apiPort); err != nil {
  210. return err
  211. }
  212. proc := newProcess(cfgPath, fmt.Sprintf("inbound %d", inst.Id))
  213. if err := proc.Start(); err != nil {
  214. return err
  215. }
  216. m.procs[inst.Id] = &managed{
  217. proc: proc,
  218. tag: inst.Tag,
  219. fingerprint: fp,
  220. apiPort: apiPort,
  221. last: map[string]clientCounters{},
  222. }
  223. logger.Infof("mtproto: started mtg for inbound %d on %s", inst.Id, inst.bindTo())
  224. return nil
  225. }
  226. // Remove stops and forgets the mtg process for an inbound id.
  227. func (m *Manager) Remove(id int) {
  228. m.mu.Lock()
  229. defer m.mu.Unlock()
  230. if cur, ok := m.procs[id]; ok {
  231. _ = cur.proc.Stop()
  232. delete(m.procs, id)
  233. _ = os.Remove(configPathForID(id))
  234. logger.Infof("mtproto: stopped mtg for inbound %d", id)
  235. }
  236. }
  237. // Reconcile drives the running set toward the desired instances: it stops
  238. // processes that are no longer wanted and (re)starts the rest. Used at boot
  239. // and periodically to recover from crashes.
  240. func (m *Manager) Reconcile(desired []Instance) {
  241. m.mu.Lock()
  242. defer m.mu.Unlock()
  243. m.sweepOrphansLocked()
  244. want := make(map[int]struct{}, len(desired))
  245. for _, inst := range desired {
  246. want[inst.Id] = struct{}{}
  247. }
  248. for id, cur := range m.procs {
  249. if _, ok := want[id]; !ok {
  250. _ = cur.proc.Stop()
  251. delete(m.procs, id)
  252. _ = os.Remove(configPathForID(id))
  253. }
  254. }
  255. for _, inst := range desired {
  256. if err := m.ensureLocked(inst); err != nil {
  257. logger.Warningf("mtproto: reconcile failed for inbound %d: %v", inst.Id, err)
  258. }
  259. }
  260. }
  261. // StopAll stops every managed mtg process. Called on panel shutdown.
  262. func (m *Manager) StopAll() {
  263. m.mu.Lock()
  264. defer m.mu.Unlock()
  265. for id, cur := range m.procs {
  266. _ = cur.proc.Stop()
  267. _ = os.Remove(configPathForID(id))
  268. delete(m.procs, id)
  269. }
  270. }
  271. // CollectTraffic scrapes each running mtg /stats endpoint and returns the
  272. // per-client byte deltas since the previous scrape, plus the emails of clients
  273. // with at least one live connection.
  274. func (m *Manager) CollectTraffic() ([]Traffic, []string) {
  275. type snap struct {
  276. id int
  277. apiPort int
  278. tag string
  279. last map[string]clientCounters
  280. }
  281. m.mu.Lock()
  282. snaps := make([]snap, 0, len(m.procs))
  283. for id, cur := range m.procs {
  284. if cur.proc == nil || !cur.proc.IsRunning() {
  285. continue
  286. }
  287. lastCopy := make(map[string]clientCounters, len(cur.last))
  288. for k, v := range cur.last {
  289. lastCopy[k] = v
  290. }
  291. snaps = append(snaps, snap{id: id, apiPort: cur.apiPort, tag: cur.tag, last: lastCopy})
  292. }
  293. m.mu.Unlock()
  294. var out []Traffic
  295. var online []string
  296. for _, s := range snaps {
  297. users, ok := scrapeStats(s.apiPort)
  298. if !ok {
  299. continue
  300. }
  301. newLast := make(map[string]clientCounters, len(users))
  302. for email, u := range users {
  303. up := u.BytesIn
  304. down := u.BytesOut
  305. newLast[email] = clientCounters{up: up, down: down}
  306. if u.Connections > 0 {
  307. online = append(online, email)
  308. }
  309. prev, had := s.last[email]
  310. if !had {
  311. continue
  312. }
  313. du := up - prev.up
  314. dd := down - prev.down
  315. if du < 0 {
  316. du = 0
  317. }
  318. if dd < 0 {
  319. dd = 0
  320. }
  321. if du > 0 || dd > 0 {
  322. out = append(out, Traffic{Tag: s.tag, Email: email, Up: du, Down: dd})
  323. }
  324. }
  325. m.mu.Lock()
  326. if cur, ok := m.procs[s.id]; ok {
  327. cur.last = newLast
  328. }
  329. m.mu.Unlock()
  330. }
  331. return out, online
  332. }
  333. // FreeLocalPort asks the OS for an unused loopback TCP port. It is used both
  334. // for mtg's /stats API endpoint and to allocate the per-inbound SOCKS egress
  335. // bridge port persisted into mtproto inbound settings.
  336. func FreeLocalPort() (int, error) {
  337. l, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", "127.0.0.1:0")
  338. if err != nil {
  339. return 0, err
  340. }
  341. defer l.Close()
  342. return l.Addr().(*net.TCPAddr).Port, nil
  343. }
  344. // renderConfig builds the mtg-multi TOML for an instance. Top-level keys must
  345. // precede any [section] header in TOML, and [secrets] must be the final section
  346. // so trailing keys are not swallowed by another table. The layout is therefore:
  347. // top-level scalars (incl. api-bind-to), then [domain-fronting], [network] and
  348. // [throttle], and finally [secrets] with one named secret per active client.
  349. func renderConfig(inst Instance, apiPort int) string {
  350. var b strings.Builder
  351. fmt.Fprintf(&b, "bind-to = %q\n", inst.bindTo())
  352. if inst.Debug {
  353. b.WriteString("debug = true\n")
  354. }
  355. if inst.ProxyProtocolListener {
  356. b.WriteString("proxy-protocol-listener = true\n")
  357. }
  358. if inst.PreferIP != "" {
  359. fmt.Fprintf(&b, "prefer-ip = %q\n", inst.PreferIP)
  360. }
  361. fmt.Fprintf(&b, "api-bind-to = \"127.0.0.1:%d\"\n", apiPort)
  362. if inst.FrontingIP != "" || inst.FrontingPort > 0 || inst.FrontingProxyProtocol {
  363. b.WriteString("\n[domain-fronting]\n")
  364. if inst.FrontingIP != "" {
  365. fmt.Fprintf(&b, "host = %q\n", inst.FrontingIP)
  366. }
  367. if inst.FrontingPort > 0 {
  368. fmt.Fprintf(&b, "port = %d\n", inst.FrontingPort)
  369. }
  370. if inst.FrontingProxyProtocol {
  371. b.WriteString("proxy-protocol = true\n")
  372. }
  373. }
  374. // When the inbound opts into Xray routing, mtg reaches Telegram through the
  375. // loopback SOCKS bridge the panel injects into the running Xray config. mtg
  376. // only supports SOCKS5 upstreams, which is exactly what the bridge exposes.
  377. if inst.RouteThroughXray && inst.XrayRoutePort > 0 {
  378. fmt.Fprintf(&b, "\n[network]\nproxies = [\"socks5://127.0.0.1:%d\"]\n", inst.XrayRoutePort)
  379. }
  380. if inst.ThrottleMaxConnections > 0 {
  381. fmt.Fprintf(&b, "\n[throttle]\nmax-connections = %d\n", inst.ThrottleMaxConnections)
  382. }
  383. b.WriteString("\n[secrets]\n")
  384. for _, e := range inst.Secrets {
  385. fmt.Fprintf(&b, "%q = %q\n", e.Name, e.Secret)
  386. }
  387. return b.String()
  388. }
  389. func writeConfig(path string, inst Instance, apiPort int) error {
  390. if err := os.MkdirAll(configDir(), 0o750); err != nil {
  391. return err
  392. }
  393. return os.WriteFile(path, []byte(renderConfig(inst, apiPort)), 0o640)
  394. }
  395. // statsUser is one entry of the mtg-multi /stats users map. bytes_in is traffic
  396. // the client sent to the proxy (upload) and bytes_out is what the proxy returned
  397. // (download).
  398. type statsUser struct {
  399. Connections int64 `json:"connections"`
  400. BytesIn int64 `json:"bytes_in"`
  401. BytesOut int64 `json:"bytes_out"`
  402. }
  403. // scrapeStats reads the mtg-multi /stats JSON API and returns the per-user
  404. // cumulative counters. Best-effort: an unreachable endpoint or unparseable body
  405. // yields ok=false.
  406. func scrapeStats(port int) (map[string]statsUser, bool) {
  407. client := http.Client{Timeout: 3 * time.Second}
  408. req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf("http://127.0.0.1:%d/stats", port), nil)
  409. if err != nil {
  410. return nil, false
  411. }
  412. resp, err := client.Do(req)
  413. if err != nil {
  414. return nil, false
  415. }
  416. defer resp.Body.Close()
  417. var parsed struct {
  418. Users map[string]statsUser `json:"users"`
  419. }
  420. if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
  421. return nil, false
  422. }
  423. return parsed.Users, true
  424. }