1
0

manager.go 20 KB

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