inbound_disable.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. package service
  2. import (
  3. "encoding/json"
  4. "slices"
  5. "time"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  8. "gorm.io/gorm"
  9. )
  10. func (s *InboundService) disableInvalidInbounds(tx *gorm.DB, mutationBatch *trafficMutationBatch) (bool, int64, error) {
  11. now := time.Now().Unix() * 1000
  12. var inbounds []model.Inbound
  13. if err := tx.Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ? and node_id IS NULL", now, true).
  14. Find(&inbounds).Error; err != nil {
  15. return false, 0, err
  16. }
  17. for i := range inbounds {
  18. mutationBatch.localPlans = append(mutationBatch.localPlans, trafficLocalApplyPlan{
  19. action: trafficDisableInbound, inbound: inbounds[i],
  20. })
  21. }
  22. result := tx.Model(model.Inbound{}).
  23. Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ? and node_id IS NULL", now, true).
  24. Update("enable", false)
  25. err := result.Error
  26. count := result.RowsAffected
  27. return false, count, err
  28. }
  29. const globalTrafficFreshWindow = 24 * time.Hour
  30. func globalTrafficFreshSince() int64 {
  31. return time.Now().Add(-globalTrafficFreshWindow).UnixMilli()
  32. }
  33. // depletedClientsCond matches clients that exhausted their quota or expired.
  34. // Besides the local counters it also trips on the cross-panel usage a master
  35. // pushed into client_global_traffics — that's what lets a node cut a client
  36. // whose combined usage exceeds the quota even though the local share doesn't.
  37. // Only rows a master refreshed recently count (placeholders: now, freshSince).
  38. const depletedClientsCond = `((total > 0 AND up + down >= total)
  39. OR (expiry_time > 0 AND expiry_time <= ?)
  40. OR (total > 0 AND EXISTS (
  41. SELECT 1 FROM client_global_traffics g
  42. WHERE g.email = client_traffics.email
  43. AND g.updated_at >= ?
  44. AND g.up + g.down >= client_traffics.total
  45. )))`
  46. // depletedClientsCondLocal is depletedClientsCond without the cross-panel
  47. // client_global_traffics check. The EXISTS branch is a correlated subquery that
  48. // turns every traffic poll into a full client_traffics scan; on a panel no
  49. // master pushes to (the common case) client_global_traffics is empty, so the
  50. // branch can never match and is pure CPU cost (#5392). Placeholders: now.
  51. const depletedClientsCondLocal = `((total > 0 AND up + down >= total)
  52. OR (expiry_time > 0 AND expiry_time <= ?))`
  53. // depletedCond returns the predicate matching depleted clients together with
  54. // the arguments it binds. The local-only variant is used unless this panel
  55. // holds a global-traffic row a master still refreshes, in which case the
  56. // cross-panel EXISTS check is needed to enforce combined quota.
  57. func depletedCond(tx *gorm.DB) (string, []any) {
  58. now := time.Now().UnixMilli()
  59. freshSince := globalTrafficFreshSince()
  60. var probe int64
  61. err := tx.Model(&model.ClientGlobalTraffic{}).
  62. Where("updated_at >= ?", freshSince).
  63. Limit(1).Count(&probe).Error
  64. if err == nil && probe > 0 {
  65. return depletedClientsCond, []any{now, freshSince}
  66. }
  67. return depletedClientsCondLocal, []any{now}
  68. }
  69. func (s *InboundService) disableInvalidClients(tx *gorm.DB, mutationBatch *trafficMutationBatch) (bool, int64, []int, error) {
  70. now := time.Now().UnixMilli()
  71. cond, condArgs := depletedCond(tx)
  72. var depletedRows []xray.ClientTraffic
  73. err := tx.Model(xray.ClientTraffic{}).
  74. Where(cond+" AND enable = ?", append(condArgs, true)...).
  75. Find(&depletedRows).Error
  76. if err != nil {
  77. return false, 0, nil, err
  78. }
  79. if len(depletedRows) == 0 {
  80. return false, 0, nil, nil
  81. }
  82. depletedEmails := make([]string, 0, len(depletedRows))
  83. for i := range depletedRows {
  84. if depletedRows[i].Email == "" {
  85. continue
  86. }
  87. depletedEmails = append(depletedEmails, depletedRows[i].Email)
  88. }
  89. type target struct {
  90. InboundID int `gorm:"column:inbound_id"`
  91. NodeID *int `gorm:"column:node_id"`
  92. Tag string
  93. Email string
  94. }
  95. var targets []target
  96. if len(depletedEmails) > 0 {
  97. err = tx.Raw(`
  98. SELECT inbounds.id AS inbound_id, inbounds.node_id AS node_id,
  99. inbounds.tag AS tag, clients.email AS email
  100. FROM clients
  101. JOIN client_inbounds ON client_inbounds.client_id = clients.id
  102. JOIN inbounds ON inbounds.id = client_inbounds.inbound_id
  103. WHERE clients.email IN ?
  104. `, depletedEmails).Scan(&targets).Error
  105. if err != nil {
  106. return false, 0, nil, err
  107. }
  108. }
  109. byInbound := make(map[int][]target)
  110. for _, t := range targets {
  111. byInbound[t.InboundID] = append(byInbound[t.InboundID], t)
  112. }
  113. disabledNodeIDs := make(map[int]struct{})
  114. for inboundID, group := range byInbound {
  115. emails := make(map[string]struct{}, len(group))
  116. for _, t := range group {
  117. emails[t.Email] = struct{}{}
  118. }
  119. oldInbound, inbound, mErr := s.markClientsDisabledInSettings(tx, inboundID, emails)
  120. if mErr != nil {
  121. return false, 0, nil, mErr
  122. }
  123. if inbound.NodeID != nil {
  124. mutationBatch.remotePlans = append(mutationBatch.remotePlans, trafficInboundUpdatePlan{
  125. oldInbound: *oldInbound, newInbound: *inbound,
  126. })
  127. mutationBatch.addNode(*inbound.NodeID)
  128. disabledNodeIDs[*inbound.NodeID] = struct{}{}
  129. continue
  130. }
  131. for email := range emails {
  132. mutationBatch.localPlans = append(mutationBatch.localPlans, trafficLocalApplyPlan{
  133. action: trafficRemoveUser, inbound: *inbound, email: email,
  134. })
  135. }
  136. }
  137. // Flip the rows already collected above by primary key instead of
  138. // re-evaluating the depleted predicate, which was a second full scan of
  139. // client_traffics on every poll. Sorted ids keep the lock order stable.
  140. ids := make([]int, 0, len(depletedRows))
  141. for i := range depletedRows {
  142. ids = append(ids, depletedRows[i].Id)
  143. }
  144. slices.Sort(ids)
  145. var count int64
  146. for _, batch := range chunkInts(ids, sqlInChunk) {
  147. result := tx.Model(xray.ClientTraffic{}).
  148. Where("id IN ? AND enable = ?", batch, true).
  149. Update("enable", false)
  150. if result.Error != nil {
  151. return false, count, nil, result.Error
  152. }
  153. count += result.RowsAffected
  154. }
  155. if len(depletedEmails) > 0 {
  156. if err := tx.Model(&model.ClientRecord{}).
  157. Where("email IN ?", depletedEmails).
  158. Updates(map[string]any{"enable": false, "updated_at": now}).Error; err != nil {
  159. return false, count, nil, err
  160. }
  161. }
  162. nodeIDs := make([]int, 0, len(disabledNodeIDs))
  163. for nodeID := range disabledNodeIDs {
  164. nodeIDs = append(nodeIDs, nodeID)
  165. }
  166. return false, count, nodeIDs, nil
  167. }
  168. // markClientsDisabledInSettings flips client.enable=false in the inbound's
  169. // stored settings JSON for the given emails and returns both the pre and
  170. // post snapshots so a caller pushing to a remote node has the diff to hand.
  171. func (s *InboundService) markClientsDisabledInSettings(tx *gorm.DB, inboundID int, emails map[string]struct{}) (oldIb, newIb *model.Inbound, err error) {
  172. var ib model.Inbound
  173. if err := tx.Model(&model.Inbound{}).Where("id = ?", inboundID).First(&ib).Error; err != nil {
  174. return nil, nil, err
  175. }
  176. snapshot := ib
  177. settings := map[string]any{}
  178. if err := json.Unmarshal([]byte(ib.Settings), &settings); err != nil {
  179. return nil, nil, err
  180. }
  181. clients, _ := settings["clients"].([]any)
  182. now := time.Now().Unix() * 1000
  183. mutated := false
  184. for i := range clients {
  185. entry, ok := clients[i].(map[string]any)
  186. if !ok {
  187. continue
  188. }
  189. email, _ := entry["email"].(string)
  190. if _, hit := emails[email]; !hit {
  191. continue
  192. }
  193. if cur, _ := entry["enable"].(bool); !cur {
  194. continue
  195. }
  196. entry["enable"] = false
  197. entry["updated_at"] = now
  198. clients[i] = entry
  199. mutated = true
  200. }
  201. if !mutated {
  202. return &snapshot, &ib, nil
  203. }
  204. settings["clients"] = clients
  205. bs, marshalErr := json.MarshalIndent(settings, "", " ")
  206. if marshalErr != nil {
  207. return nil, nil, marshalErr
  208. }
  209. ib.Settings = string(bs)
  210. if err := tx.Model(&model.Inbound{}).Where("id = ?", inboundID).
  211. Update("settings", ib.Settings).Error; err != nil {
  212. return nil, nil, err
  213. }
  214. return &snapshot, &ib, nil
  215. }