inbound_client_ips.go 11 KB

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