local.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. package runtime
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "sync"
  7. "github.com/mhsanaei/3x-ui/v3/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/xray"
  9. )
  10. // LocalDeps wires the runtime to the panel's xray process and the
  11. // service.XrayService restart trigger via callbacks. We use callbacks
  12. // (not an interface to *service.XrayService) because the runtime
  13. // package would otherwise cycle-import service.
  14. type LocalDeps struct {
  15. // APIPort returns the xray gRPC API port the local engine is
  16. // currently listening on. Returns 0 when xray isn't running yet —
  17. // callers should treat that as a transient error.
  18. APIPort func() int
  19. // SetNeedRestart trips the panel's "restart xray on next cron tick"
  20. // flag. Mirrors how InboundController.addInbound calls
  21. // xrayService.SetToNeedRestart() today.
  22. SetNeedRestart func()
  23. }
  24. // Local implements Runtime against the panel's own xray process. Each
  25. // call follows the existing inbound.go pattern: open a gRPC client,
  26. // run one operation, close. Per-call init keeps the connection state
  27. // scoped so a stuck call can't leak across operations.
  28. type Local struct {
  29. deps LocalDeps
  30. // Serialise gRPC operations — xray's HandlerService isn't documented
  31. // as concurrent-safe and the existing InboundService implicitly
  32. // runs one op at a time per request. This matches that.
  33. mu sync.Mutex
  34. }
  35. // NewLocal builds a Local runtime. deps.APIPort and deps.SetNeedRestart
  36. // are required; callers that want a no-op restart can pass `func(){}`.
  37. func NewLocal(deps LocalDeps) *Local {
  38. return &Local{deps: deps}
  39. }
  40. func (l *Local) Name() string { return "local" }
  41. // withAPI runs fn against a freshly-initialised XrayAPI client and
  42. // guarantees Close() afterwards. Returns an error if the gRPC port
  43. // isn't available yet (xray still starting / stopped).
  44. func (l *Local) withAPI(fn func(api *xray.XrayAPI) error) error {
  45. l.mu.Lock()
  46. defer l.mu.Unlock()
  47. port := l.deps.APIPort()
  48. if port <= 0 {
  49. return errors.New("local xray is not running")
  50. }
  51. var api xray.XrayAPI
  52. if err := api.Init(port); err != nil {
  53. return err
  54. }
  55. defer api.Close()
  56. return fn(&api)
  57. }
  58. func (l *Local) AddInbound(_ context.Context, ib *model.Inbound) error {
  59. body, err := json.MarshalIndent(ib.GenXrayInboundConfig(), "", " ")
  60. if err != nil {
  61. return err
  62. }
  63. return l.withAPI(func(api *xray.XrayAPI) error {
  64. return api.AddInbound(body)
  65. })
  66. }
  67. func (l *Local) DelInbound(_ context.Context, ib *model.Inbound) error {
  68. return l.withAPI(func(api *xray.XrayAPI) error {
  69. return api.DelInbound(ib.Tag)
  70. })
  71. }
  72. func (l *Local) UpdateInbound(ctx context.Context, oldIb, newIb *model.Inbound) error {
  73. // xray-core has no in-place inbound update — drop and re-add.
  74. // Matches what InboundService.UpdateInbound did inline.
  75. if err := l.DelInbound(ctx, oldIb); err != nil {
  76. // Best-effort: continue to AddInbound so a transient remove
  77. // failure (e.g. inbound already gone) doesn't strand us. The
  78. // caller's needRestart fallback will reconcile from config.
  79. _ = err
  80. }
  81. if !newIb.Enable {
  82. // Disabled inbounds aren't pushed to xray; we already removed
  83. // the old one above.
  84. return nil
  85. }
  86. return l.AddInbound(ctx, newIb)
  87. }
  88. func (l *Local) AddUser(_ context.Context, ib *model.Inbound, userMap map[string]any) error {
  89. return l.withAPI(func(api *xray.XrayAPI) error {
  90. return api.AddUser(string(ib.Protocol), ib.Tag, userMap)
  91. })
  92. }
  93. func (l *Local) RemoveUser(_ context.Context, ib *model.Inbound, email string) error {
  94. return l.withAPI(func(api *xray.XrayAPI) error {
  95. return api.RemoveUser(ib.Tag, email)
  96. })
  97. }
  98. func (l *Local) RestartXray(_ context.Context) error {
  99. if l.deps.SetNeedRestart != nil {
  100. l.deps.SetNeedRestart()
  101. }
  102. return nil
  103. }
  104. // Reset methods are intentional no-ops for Local. The central DB UPDATE
  105. // that runs in InboundService.Reset* before this call has already zeroed
  106. // the counters that xray reads; on the next stats poll the gRPC service
  107. // will pick up matching values. Pre-Phase-1 the panel never issued an
  108. // xrayApi reset call here either — keeping the same shape avoids a
  109. // behaviour change for single-panel users.
  110. func (l *Local) ResetClientTraffic(_ context.Context, _ *model.Inbound, _ string) error {
  111. return nil
  112. }
  113. func (l *Local) ResetInboundClientTraffics(_ context.Context, _ *model.Inbound) error {
  114. return nil
  115. }
  116. func (l *Local) ResetAllTraffics(_ context.Context) error {
  117. return nil
  118. }