inbound_client_ips.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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. // nodeHostedEmails is every client one node serves, its descendants' included.
  18. // Per-node pushes are scoped to it so their cost tracks the node, not the fleet.
  19. func nodeHostedEmails(db *gorm.DB, nodeID int) ([]string, error) {
  20. var emails []string
  21. err := db.Model(&model.NodeClientTraffic{}).Where("node_id = ?", nodeID).Pluck("email", &emails).Error
  22. return emails, err
  23. }
  24. // GetNodeInboundClientIps returns the IP rows of the clients nodeID hosts: a node's
  25. // IP-limit job reads no other row, so pushing the rest only made it echo them back.
  26. func (s *InboundService) GetNodeInboundClientIps(nodeID int) ([]model.InboundClientIps, error) {
  27. db := database.GetDB()
  28. emails, err := nodeHostedEmails(db, nodeID)
  29. if err != nil || len(emails) == 0 {
  30. return nil, err
  31. }
  32. var ips []model.InboundClientIps
  33. for _, batch := range chunkStrings(emails, sqlInChunk) {
  34. var page []model.InboundClientIps
  35. if err := db.Where("client_email IN ?", batch).Find(&page).Error; err != nil {
  36. return nil, err
  37. }
  38. ips = append(ips, page...)
  39. }
  40. return ips, nil
  41. }
  42. // clientIpStaleAfterSeconds mirrors job.ipStaleAfterSeconds: client IPs older than
  43. // 30 minutes are evicted. Applying the same cutoff inside the cross-node merge keeps
  44. // the synced blob bounded and stops the master's push-back from resurrecting IPs that
  45. // a node has already pruned (otherwise the merge defeats the eviction cluster-wide).
  46. const clientIpStaleAfterSeconds = int64(30 * 60)
  47. // clientIpEntry is the on-disk shape of each element of InboundClientIps.Ips. Tags
  48. // match job.IPWithTimestamp so the blob round-trips with the access.log scanner.
  49. type clientIpEntry struct {
  50. IP string `json:"ip"`
  51. Timestamp int64 `json:"timestamp"`
  52. }
  53. // mergeClientIpEntries unions old and incoming IP observations, dropping anything
  54. // older than cutoff, keeping the most recent timestamp per IP, and returning the
  55. // result sorted newest-first.
  56. func mergeClientIpEntries(old, incoming []clientIpEntry, cutoff int64) []clientIpEntry {
  57. ipMap := make(map[string]int64, len(old)+len(incoming))
  58. for _, e := range old {
  59. if e.Timestamp < cutoff {
  60. continue
  61. }
  62. ipMap[e.IP] = e.Timestamp
  63. }
  64. for _, e := range incoming {
  65. if e.Timestamp < cutoff {
  66. continue
  67. }
  68. if cur, ok := ipMap[e.IP]; !ok || e.Timestamp > cur {
  69. ipMap[e.IP] = e.Timestamp
  70. }
  71. }
  72. out := make([]clientIpEntry, 0, len(ipMap))
  73. for ip, ts := range ipMap {
  74. out = append(out, clientIpEntry{IP: ip, Timestamp: ts})
  75. }
  76. sort.Slice(out, func(i, j int) bool { return out[i].Timestamp > out[j].Timestamp })
  77. return out
  78. }
  79. // MergeInboundClientIps folds client IPs synced from another node into the local
  80. // inbound_client_ips table without double-counting an IP seen on multiple nodes and
  81. // without resurrecting stale entries. Existing rows are updated in place; brand-new
  82. // clients (typically node-only clients with no local row) are created with a fresh
  83. // local id.
  84. func (s *InboundService) MergeInboundClientIps(incomingIps []model.InboundClientIps) error {
  85. db := database.GetDB()
  86. var currentIps []model.InboundClientIps
  87. if err := db.Model(&model.InboundClientIps{}).Find(&currentIps).Error; err != nil {
  88. return err
  89. }
  90. currentMap := make(map[string]*model.InboundClientIps, len(currentIps))
  91. for i := range currentIps {
  92. currentMap[currentIps[i].ClientEmail] = &currentIps[i]
  93. }
  94. now := time.Now().Unix()
  95. cutoff := now - clientIpStaleAfterSeconds
  96. // Node syncs run concurrently (one goroutine per node) and shared clients
  97. // appear in several nodes' reports. Locking rows in each node's arbitrary
  98. // report order lets two merges grab the same rows in opposite order, which
  99. // Postgres aborts as a deadlock (40P01) — take them in one global order.
  100. sort.Slice(incomingIps, func(i, j int) bool {
  101. return incomingIps[i].ClientEmail < incomingIps[j].ClientEmail
  102. })
  103. tx := db.Begin()
  104. defer func() {
  105. if r := recover(); r != nil {
  106. tx.Rollback()
  107. }
  108. }()
  109. for _, incoming := range incomingIps {
  110. if incoming.ClientEmail == "" || incoming.Ips == "" {
  111. continue
  112. }
  113. var incomingEntries []clientIpEntry
  114. _ = json.Unmarshal([]byte(incoming.Ips), &incomingEntries)
  115. current, exists := currentMap[incoming.ClientEmail]
  116. if !exists {
  117. // New client we've never seen locally. Drop stale entries up front and
  118. // skip the row entirely if nothing is fresh, so we don't persist a row
  119. // that is dead on arrival.
  120. fresh := mergeClientIpEntries(nil, incomingEntries, cutoff)
  121. if len(fresh) == 0 {
  122. continue
  123. }
  124. b, _ := json.Marshal(fresh)
  125. incoming.Ips = string(b)
  126. // Never carry the remote node's primary key into the local table: id
  127. // spaces are independent across nodes and the remote id would collide
  128. // with an unrelated local row. OnConflict guards the race where
  129. // check_client_ip_job creates the same brand-new email between the
  130. // snapshot above and this insert.
  131. incoming.Id = 0
  132. if err := tx.Clauses(clause.OnConflict{
  133. Columns: []clause.Column{{Name: "client_email"}},
  134. DoNothing: true,
  135. }).Create(&incoming).Error; err != nil {
  136. tx.Rollback()
  137. return err
  138. }
  139. continue
  140. }
  141. var oldEntries []clientIpEntry
  142. if current.Ips != "" {
  143. _ = json.Unmarshal([]byte(current.Ips), &oldEntries)
  144. }
  145. merged := mergeClientIpEntries(oldEntries, incomingEntries, cutoff)
  146. b, _ := json.Marshal(merged)
  147. mergedStr := string(b)
  148. // A concurrent check_client_ip_job db.Save on the same row can interleave
  149. // with this update (benign last-writer-wins; any dropped IP reappears on the
  150. // next scan/sync), so only write when the blob actually changed.
  151. if current.Ips != mergedStr {
  152. if err := tx.Model(&model.InboundClientIps{}).Where("id = ?", current.Id).Update("ips", mergedStr).Error; err != nil {
  153. tx.Rollback()
  154. return err
  155. }
  156. }
  157. }
  158. return tx.Commit().Error
  159. }
  160. func (s *InboundService) UpdateClientIPs(tx *gorm.DB, oldEmail string, newEmail string) error {
  161. // The caller only renames onto a free identity, so a row already sitting on
  162. // newEmail is stale tracking data — drop it instead of failing the edit.
  163. if oldEmail != newEmail {
  164. if err := tx.Where("client_email = ?", newEmail).Delete(model.InboundClientIps{}).Error; err != nil {
  165. return err
  166. }
  167. }
  168. return tx.Model(model.InboundClientIps{}).Where("client_email = ?", oldEmail).Update("client_email", newEmail).Error
  169. }
  170. func (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error {
  171. return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error
  172. }
  173. func (s *InboundService) delClientIPsByEmails(tx *gorm.DB, emails []string) error {
  174. const chunk = 400
  175. for start := 0; start < len(emails); start += chunk {
  176. end := min(start+chunk, len(emails))
  177. if err := tx.Where("client_email IN ?", emails[start:end]).Delete(model.InboundClientIps{}).Error; err != nil {
  178. return err
  179. }
  180. }
  181. return nil
  182. }
  183. func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) {
  184. db := database.GetDB()
  185. InboundClientIps := &model.InboundClientIps{}
  186. err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
  187. if err != nil {
  188. return "", err
  189. }
  190. if InboundClientIps.Ips == "" {
  191. return "", nil
  192. }
  193. // Try to parse as new format (with timestamps)
  194. type IPWithTimestamp struct {
  195. IP string `json:"ip"`
  196. Timestamp int64 `json:"timestamp"`
  197. }
  198. var ipsWithTime []IPWithTimestamp
  199. err = json.Unmarshal([]byte(InboundClientIps.Ips), &ipsWithTime)
  200. // If successfully parsed as new format, return with timestamps
  201. if err == nil && len(ipsWithTime) > 0 {
  202. return InboundClientIps.Ips, nil
  203. }
  204. // Otherwise, assume it's old format (simple string array)
  205. // Try to parse as simple array and convert to new format
  206. var oldIps []string
  207. err = json.Unmarshal([]byte(InboundClientIps.Ips), &oldIps)
  208. if err == nil && len(oldIps) > 0 {
  209. // Convert old format to new format with current timestamp
  210. newIpsWithTime := make([]IPWithTimestamp, len(oldIps))
  211. for i, ip := range oldIps {
  212. newIpsWithTime[i] = IPWithTimestamp{
  213. IP: ip,
  214. Timestamp: time.Now().Unix(),
  215. }
  216. }
  217. result, _ := json.Marshal(newIpsWithTime)
  218. return string(result), nil
  219. }
  220. // Return as-is if parsing fails
  221. return InboundClientIps.Ips, nil
  222. }
  223. func (s *InboundService) ClearClientIps(clientEmail string) error {
  224. db := database.GetDB()
  225. result := db.Model(model.InboundClientIps{}).
  226. Where("client_email = ?", clientEmail).
  227. Update("ips", "")
  228. err := result.Error
  229. if err != nil {
  230. return err
  231. }
  232. return nil
  233. }
  234. // PruneStaleClientIps enforces clientIpStaleAfterSeconds for rows the online
  235. // scan no longer rewrites: an offline client's addresses must still expire.
  236. func (s *InboundService) PruneStaleClientIps() error {
  237. db := database.GetDB()
  238. cutoff := time.Now().Unix() - clientIpStaleAfterSeconds
  239. var rows []model.InboundClientIps
  240. if err := db.Find(&rows).Error; err != nil {
  241. return err
  242. }
  243. for _, row := range rows {
  244. var entries []clientIpEntry
  245. if row.Ips != "" {
  246. // Legacy blobs without timestamps stay untouched; the next scan rewrites them.
  247. if err := json.Unmarshal([]byte(row.Ips), &entries); err != nil {
  248. continue
  249. }
  250. }
  251. kept := mergeClientIpEntries(nil, entries, cutoff)
  252. if len(kept) == 0 {
  253. if err := db.Delete(&model.InboundClientIps{}, row.Id).Error; err != nil {
  254. return err
  255. }
  256. continue
  257. }
  258. if len(kept) == len(entries) {
  259. continue
  260. }
  261. b, _ := json.Marshal(kept)
  262. if err := db.Model(&model.InboundClientIps{}).Where("id = ?", row.Id).Update("ips", string(b)).Error; err != nil {
  263. return err
  264. }
  265. }
  266. return pruneStaleNodeClientIps(cutoff)
  267. }