inbound_client_ips.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. package service
  2. import (
  3. "encoding/json"
  4. "sort"
  5. "time"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. "gorm.io/gorm"
  9. "gorm.io/gorm/clause"
  10. )
  11. func (s *InboundService) GetAllInboundClientIps() ([]model.InboundClientIps, error) {
  12. db := database.GetDB()
  13. var ips []model.InboundClientIps
  14. err := db.Model(&model.InboundClientIps{}).Find(&ips).Error
  15. return ips, err
  16. }
  17. // clientIpStaleAfterSeconds mirrors job.ipStaleAfterSeconds: client IPs older than
  18. // 30 minutes are evicted. Applying the same cutoff inside the cross-node merge keeps
  19. // the synced blob bounded and stops the master's push-back from resurrecting IPs that
  20. // a node has already pruned (otherwise the merge defeats the eviction cluster-wide).
  21. const clientIpStaleAfterSeconds = int64(30 * 60)
  22. // clientIpEntry is the on-disk shape of each element of InboundClientIps.Ips. Tags
  23. // match job.IPWithTimestamp so the blob round-trips with the access.log scanner.
  24. type clientIpEntry struct {
  25. IP string `json:"ip"`
  26. Timestamp int64 `json:"timestamp"`
  27. }
  28. // mergeClientIpEntries unions old and incoming IP observations, dropping anything
  29. // older than cutoff, keeping the most recent timestamp per IP, and returning the
  30. // result sorted newest-first.
  31. func mergeClientIpEntries(old, incoming []clientIpEntry, cutoff int64) []clientIpEntry {
  32. ipMap := make(map[string]int64, len(old)+len(incoming))
  33. for _, e := range old {
  34. if e.Timestamp < cutoff {
  35. continue
  36. }
  37. ipMap[e.IP] = e.Timestamp
  38. }
  39. for _, e := range incoming {
  40. if e.Timestamp < cutoff {
  41. continue
  42. }
  43. if cur, ok := ipMap[e.IP]; !ok || e.Timestamp > cur {
  44. ipMap[e.IP] = e.Timestamp
  45. }
  46. }
  47. out := make([]clientIpEntry, 0, len(ipMap))
  48. for ip, ts := range ipMap {
  49. out = append(out, clientIpEntry{IP: ip, Timestamp: ts})
  50. }
  51. sort.Slice(out, func(i, j int) bool { return out[i].Timestamp > out[j].Timestamp })
  52. return out
  53. }
  54. // MergeInboundClientIps folds client IPs synced from another node into the local
  55. // inbound_client_ips table without double-counting an IP seen on multiple nodes and
  56. // without resurrecting stale entries. Existing rows are updated in place; brand-new
  57. // clients (typically node-only clients with no local row) are created with a fresh
  58. // local id.
  59. func (s *InboundService) MergeInboundClientIps(incomingIps []model.InboundClientIps) error {
  60. db := database.GetDB()
  61. var currentIps []model.InboundClientIps
  62. if err := db.Model(&model.InboundClientIps{}).Find(&currentIps).Error; err != nil {
  63. return err
  64. }
  65. currentMap := make(map[string]*model.InboundClientIps, len(currentIps))
  66. for i := range currentIps {
  67. currentMap[currentIps[i].ClientEmail] = &currentIps[i]
  68. }
  69. now := time.Now().Unix()
  70. cutoff := now - clientIpStaleAfterSeconds
  71. // Node syncs run concurrently (one goroutine per node) and shared clients
  72. // appear in several nodes' reports. Locking rows in each node's arbitrary
  73. // report order lets two merges grab the same rows in opposite order, which
  74. // Postgres aborts as a deadlock (40P01) — take them in one global order.
  75. sort.Slice(incomingIps, func(i, j int) bool {
  76. return incomingIps[i].ClientEmail < incomingIps[j].ClientEmail
  77. })
  78. tx := db.Begin()
  79. defer func() {
  80. if r := recover(); r != nil {
  81. tx.Rollback()
  82. }
  83. }()
  84. for _, incoming := range incomingIps {
  85. if incoming.ClientEmail == "" || incoming.Ips == "" {
  86. continue
  87. }
  88. var incomingEntries []clientIpEntry
  89. _ = json.Unmarshal([]byte(incoming.Ips), &incomingEntries)
  90. current, exists := currentMap[incoming.ClientEmail]
  91. if !exists {
  92. // New client we've never seen locally. Drop stale entries up front and
  93. // skip the row entirely if nothing is fresh, so we don't persist a row
  94. // that is dead on arrival.
  95. fresh := mergeClientIpEntries(nil, incomingEntries, cutoff)
  96. if len(fresh) == 0 {
  97. continue
  98. }
  99. b, _ := json.Marshal(fresh)
  100. incoming.Ips = string(b)
  101. // Never carry the remote node's primary key into the local table: id
  102. // spaces are independent across nodes and the remote id would collide
  103. // with an unrelated local row. OnConflict guards the race where
  104. // check_client_ip_job creates the same brand-new email between the
  105. // snapshot above and this insert.
  106. incoming.Id = 0
  107. if err := tx.Clauses(clause.OnConflict{
  108. Columns: []clause.Column{{Name: "client_email"}},
  109. DoNothing: true,
  110. }).Create(&incoming).Error; err != nil {
  111. tx.Rollback()
  112. return err
  113. }
  114. continue
  115. }
  116. var oldEntries []clientIpEntry
  117. if current.Ips != "" {
  118. _ = json.Unmarshal([]byte(current.Ips), &oldEntries)
  119. }
  120. merged := mergeClientIpEntries(oldEntries, incomingEntries, cutoff)
  121. b, _ := json.Marshal(merged)
  122. mergedStr := string(b)
  123. // A concurrent check_client_ip_job db.Save on the same row can interleave
  124. // with this update (benign last-writer-wins; any dropped IP reappears on the
  125. // next scan/sync), so only write when the blob actually changed.
  126. if current.Ips != mergedStr {
  127. if err := tx.Model(&model.InboundClientIps{}).Where("id = ?", current.Id).Update("ips", mergedStr).Error; err != nil {
  128. tx.Rollback()
  129. return err
  130. }
  131. }
  132. }
  133. return tx.Commit().Error
  134. }
  135. func (s *InboundService) UpdateClientIPs(tx *gorm.DB, oldEmail string, newEmail string) error {
  136. // The caller only renames onto a free identity, so a row already sitting on
  137. // newEmail is stale tracking data — drop it instead of failing the edit.
  138. if oldEmail != newEmail {
  139. if err := tx.Where("client_email = ?", newEmail).Delete(model.InboundClientIps{}).Error; err != nil {
  140. return err
  141. }
  142. }
  143. return tx.Model(model.InboundClientIps{}).Where("client_email = ?", oldEmail).Update("client_email", newEmail).Error
  144. }
  145. func (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error {
  146. return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error
  147. }
  148. func (s *InboundService) delClientIPsByEmails(tx *gorm.DB, emails []string) error {
  149. const chunk = 400
  150. for start := 0; start < len(emails); start += chunk {
  151. end := min(start+chunk, len(emails))
  152. if err := tx.Where("client_email IN ?", emails[start:end]).Delete(model.InboundClientIps{}).Error; err != nil {
  153. return err
  154. }
  155. }
  156. return nil
  157. }
  158. func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) {
  159. db := database.GetDB()
  160. InboundClientIps := &model.InboundClientIps{}
  161. err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
  162. if err != nil {
  163. return "", err
  164. }
  165. if InboundClientIps.Ips == "" {
  166. return "", nil
  167. }
  168. // Try to parse as new format (with timestamps)
  169. type IPWithTimestamp struct {
  170. IP string `json:"ip"`
  171. Timestamp int64 `json:"timestamp"`
  172. }
  173. var ipsWithTime []IPWithTimestamp
  174. err = json.Unmarshal([]byte(InboundClientIps.Ips), &ipsWithTime)
  175. // If successfully parsed as new format, return with timestamps
  176. if err == nil && len(ipsWithTime) > 0 {
  177. return InboundClientIps.Ips, nil
  178. }
  179. // Otherwise, assume it's old format (simple string array)
  180. // Try to parse as simple array and convert to new format
  181. var oldIps []string
  182. err = json.Unmarshal([]byte(InboundClientIps.Ips), &oldIps)
  183. if err == nil && len(oldIps) > 0 {
  184. // Convert old format to new format with current timestamp
  185. newIpsWithTime := make([]IPWithTimestamp, len(oldIps))
  186. for i, ip := range oldIps {
  187. newIpsWithTime[i] = IPWithTimestamp{
  188. IP: ip,
  189. Timestamp: time.Now().Unix(),
  190. }
  191. }
  192. result, _ := json.Marshal(newIpsWithTime)
  193. return string(result), nil
  194. }
  195. // Return as-is if parsing fails
  196. return InboundClientIps.Ips, nil
  197. }
  198. func (s *InboundService) ClearClientIps(clientEmail string) error {
  199. db := database.GetDB()
  200. result := db.Model(model.InboundClientIps{}).
  201. Where("client_email = ?", clientEmail).
  202. Update("ips", "")
  203. err := result.Error
  204. if err != nil {
  205. return err
  206. }
  207. return nil
  208. }