inbound_clients.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. package service
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "strings"
  7. "time"
  8. "github.com/google/uuid"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database"
  10. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  11. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  12. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  13. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  14. "gorm.io/gorm"
  15. )
  16. type CopyClientsResult struct {
  17. Added []string `json:"added"`
  18. Skipped []string `json:"skipped"`
  19. Errors []string `json:"errors"`
  20. }
  21. // enrichClientStats resolves each inbound's clients from the clients table
  22. // once, fills in the UUID/SubId fields on the preloaded ClientStats, and tops
  23. // up rows owned by a sibling inbound (shared-email mode — the row is keyed on
  24. // email so it only preloads on its owning inbound). Reading identity from the
  25. // clients table keeps /inbounds/list in sync with the running Xray config
  26. // when the embedded settings JSON is stale (#6436).
  27. func (s *InboundService) enrichClientStats(db *gorm.DB, inbounds []*model.Inbound) {
  28. if len(inbounds) == 0 {
  29. return
  30. }
  31. clientsByInbound := s.backfillClientStats(db, inbounds)
  32. for i, inbound := range inbounds {
  33. clients := clientsByInbound[i]
  34. if len(clients) == 0 || len(inbound.ClientStats) == 0 {
  35. continue
  36. }
  37. cMap := make(map[string]model.Client, len(clients))
  38. for _, c := range clients {
  39. cMap[strings.ToLower(c.Email)] = c
  40. }
  41. for j := range inbound.ClientStats {
  42. email := strings.ToLower(inbound.ClientStats[j].Email)
  43. if c, ok := cMap[email]; ok {
  44. inbound.ClientStats[j].UUID = c.ID
  45. inbound.ClientStats[j].SubId = c.SubID
  46. }
  47. }
  48. }
  49. }
  50. // backfillClientStats tops up each inbound's preloaded ClientStats with rows
  51. // owned by a sibling inbound: client_traffics is keyed on email, so a client
  52. // attached to several inbounds has one row that only preloads on the inbound
  53. // it was created on. Returns the clients-table clients per inbound for reuse
  54. // (not the embedded settings JSON, which can lag the live UUID — #6436).
  55. func (s *InboundService) backfillClientStats(db *gorm.DB, inbounds []*model.Inbound) [][]model.Client {
  56. clientsByInbound := make([][]model.Client, len(inbounds))
  57. seenByInbound := make([]map[string]struct{}, len(inbounds))
  58. missing := make(map[string]struct{})
  59. for i, inbound := range inbounds {
  60. clients, _ := s.clientService.ListForInbound(db, inbound.Id)
  61. clientsByInbound[i] = clients
  62. seen := make(map[string]struct{}, len(inbound.ClientStats))
  63. for _, st := range inbound.ClientStats {
  64. if st.Email != "" {
  65. seen[strings.ToLower(st.Email)] = struct{}{}
  66. }
  67. }
  68. seenByInbound[i] = seen
  69. for _, c := range clients {
  70. if c.Email == "" {
  71. continue
  72. }
  73. if _, ok := seen[strings.ToLower(c.Email)]; !ok {
  74. missing[c.Email] = struct{}{}
  75. }
  76. }
  77. }
  78. if len(missing) > 0 {
  79. emails := make([]string, 0, len(missing))
  80. for e := range missing {
  81. emails = append(emails, e)
  82. }
  83. var extra []xray.ClientTraffic
  84. var loadErr error
  85. for _, batch := range chunkStrings(emails, sqlInChunk) {
  86. var page []xray.ClientTraffic
  87. if err := db.Model(xray.ClientTraffic{}).Where("email IN ?", batch).Find(&page).Error; err != nil {
  88. loadErr = err
  89. break
  90. }
  91. extra = append(extra, page...)
  92. }
  93. if loadErr != nil {
  94. logger.Warning("backfillClientStats:", loadErr)
  95. } else {
  96. byEmail := make(map[string]xray.ClientTraffic, len(extra))
  97. for _, st := range extra {
  98. byEmail[strings.ToLower(st.Email)] = st
  99. }
  100. for i, inbound := range inbounds {
  101. for _, c := range clientsByInbound[i] {
  102. if c.Email == "" {
  103. continue
  104. }
  105. key := strings.ToLower(c.Email)
  106. if _, ok := seenByInbound[i][key]; ok {
  107. continue
  108. }
  109. if st, ok := byEmail[key]; ok {
  110. inbound.ClientStats = append(inbound.ClientStats, st)
  111. seenByInbound[i][key] = struct{}{}
  112. }
  113. }
  114. }
  115. }
  116. }
  117. return clientsByInbound
  118. }
  119. // emailUsedByOtherInbounds reports whether email lives in any inbound other than
  120. // exceptInboundId. Inside a transaction pass tx: a pool query there starves the pool.
  121. func (s *InboundService) emailUsedByOtherInbounds(db *gorm.DB, email string, exceptInboundId int) (bool, error) {
  122. if email == "" {
  123. return false, nil
  124. }
  125. var count int64
  126. err := db.Table("client_inbounds").
  127. Joins("JOIN clients ON clients.id = client_inbounds.client_id").
  128. Where("client_inbounds.inbound_id != ? AND LOWER(clients.email) = ?",
  129. exceptInboundId, strings.ToLower(strings.TrimSpace(email))).
  130. Count(&count).Error
  131. if err != nil {
  132. return false, err
  133. }
  134. return count > 0, nil
  135. }
  136. func (s *InboundService) emailsUsedByOtherInbounds(db *gorm.DB, emails []string, exceptInboundId int) (map[string]bool, error) {
  137. shared := make(map[string]bool, len(emails))
  138. want := make(map[string]struct{}, len(emails))
  139. for _, e := range emails {
  140. e = strings.ToLower(strings.TrimSpace(e))
  141. if e != "" {
  142. want[e] = struct{}{}
  143. }
  144. }
  145. if len(want) == 0 {
  146. return shared, nil
  147. }
  148. lowered := make([]string, 0, len(want))
  149. for e := range want {
  150. lowered = append(lowered, e)
  151. }
  152. for _, batch := range chunkStrings(lowered, sqlInChunk) {
  153. var rows []struct{ Email string }
  154. err := db.Table("client_inbounds").
  155. Joins("JOIN clients ON clients.id = client_inbounds.client_id").
  156. Select("DISTINCT LOWER(clients.email) AS email").
  157. Where("client_inbounds.inbound_id != ? AND LOWER(clients.email) IN ?", exceptInboundId, batch).
  158. Scan(&rows).Error
  159. if err != nil {
  160. return nil, err
  161. }
  162. for _, r := range rows {
  163. shared[r.Email] = true
  164. }
  165. }
  166. return shared, nil
  167. }
  168. func (s *InboundService) writeBackClientSubID(sourceInboundID int, client model.Client, subID string) (bool, error) {
  169. client.SubID = subID
  170. client.UpdatedAt = time.Now().UnixMilli()
  171. if client.Email == "" {
  172. return false, common.NewError("empty client email")
  173. }
  174. settingsBytes, err := json.Marshal(map[string][]model.Client{
  175. "clients": {client},
  176. })
  177. if err != nil {
  178. return false, err
  179. }
  180. updatePayload := &model.Inbound{
  181. Id: sourceInboundID,
  182. Settings: string(settingsBytes),
  183. }
  184. return s.clientService.UpdateInboundClient(s, updatePayload, client.Email)
  185. }
  186. func (s *InboundService) generateRandomCredential(targetProtocol model.Protocol) string {
  187. switch targetProtocol {
  188. case model.VMESS, model.VLESS:
  189. return uuid.NewString()
  190. default:
  191. return strings.ReplaceAll(uuid.NewString(), "-", "")
  192. }
  193. }
  194. func (s *InboundService) buildTargetClientFromSource(source model.Client, targetInbound *model.Inbound, email string, flow string) (model.Client, error) {
  195. nowTs := time.Now().UnixMilli()
  196. target := source
  197. target.Email = email
  198. target.CreatedAt = nowTs
  199. target.UpdatedAt = nowTs
  200. target.ID = ""
  201. target.Password = ""
  202. target.Auth = ""
  203. target.Flow = ""
  204. target.Secret = ""
  205. targetProtocol := targetInbound.Protocol
  206. switch targetProtocol {
  207. case model.VMESS:
  208. target.ID = s.generateRandomCredential(targetProtocol)
  209. case model.VLESS:
  210. target.ID = s.generateRandomCredential(targetProtocol)
  211. if (flow == "xtls-rprx-vision" || flow == "xtls-rprx-vision-udp443") &&
  212. !targetInbound.DisableFlow &&
  213. inboundCanEnableTlsFlow(string(targetProtocol), targetInbound.StreamSettings, targetInbound.Settings) {
  214. target.Flow = flow
  215. }
  216. case model.Trojan, model.Shadowsocks, model.MASQUE:
  217. target.Password = s.generateRandomCredential(targetProtocol)
  218. case model.Hysteria:
  219. target.Auth = s.generateRandomCredential(targetProtocol)
  220. case model.MTProto:
  221. target.Secret = model.GenerateFakeTLSSecret(mtprotoDomainFromSettings(targetInbound.Settings))
  222. case model.TUIC:
  223. target.ID = uuid.NewString()
  224. target.Password = s.generateRandomCredential(targetProtocol)
  225. default:
  226. target.ID = s.generateRandomCredential(targetProtocol)
  227. }
  228. return target, nil
  229. }
  230. func (s *InboundService) nextAvailableCopiedEmail(originalEmail string, targetID int, occupied map[string]struct{}) string {
  231. base := fmt.Sprintf("%s_%d", originalEmail, targetID)
  232. candidate := base
  233. suffix := 0
  234. for {
  235. if _, exists := occupied[strings.ToLower(candidate)]; !exists {
  236. occupied[strings.ToLower(candidate)] = struct{}{}
  237. return candidate
  238. }
  239. suffix++
  240. candidate = fmt.Sprintf("%s_%d", base, suffix)
  241. }
  242. }
  243. func (s *InboundService) CopyInboundClients(targetInboundID int, sourceInboundID int, clientEmails []string, flow string) (*CopyClientsResult, bool, error) {
  244. result := &CopyClientsResult{
  245. Added: []string{},
  246. Skipped: []string{},
  247. Errors: []string{},
  248. }
  249. if targetInboundID == sourceInboundID {
  250. return result, false, common.NewError("source and target inbounds must be different")
  251. }
  252. targetInbound, err := s.GetInbound(targetInboundID)
  253. if err != nil {
  254. return result, false, err
  255. }
  256. sourceInbound, err := s.GetInbound(sourceInboundID)
  257. if err != nil {
  258. return result, false, err
  259. }
  260. sourceClients, err := s.GetClients(sourceInbound)
  261. if err != nil {
  262. return result, false, err
  263. }
  264. if len(sourceClients) == 0 {
  265. return result, false, nil
  266. }
  267. allowedEmails := map[string]struct{}{}
  268. if len(clientEmails) > 0 {
  269. for _, email := range clientEmails {
  270. allowedEmails[strings.ToLower(strings.TrimSpace(email))] = struct{}{}
  271. }
  272. }
  273. occupiedEmails := map[string]struct{}{}
  274. allEmails, err := s.GetAllEmails()
  275. if err != nil {
  276. return result, false, err
  277. }
  278. for _, email := range allEmails {
  279. clean := strings.Trim(email, "\"")
  280. if clean != "" {
  281. occupiedEmails[strings.ToLower(clean)] = struct{}{}
  282. }
  283. }
  284. newClients := make([]model.Client, 0)
  285. needRestart := false
  286. for _, sourceClient := range sourceClients {
  287. originalEmail := strings.TrimSpace(sourceClient.Email)
  288. if originalEmail == "" {
  289. continue
  290. }
  291. if len(allowedEmails) > 0 {
  292. if _, ok := allowedEmails[strings.ToLower(originalEmail)]; !ok {
  293. continue
  294. }
  295. }
  296. if sourceClient.SubID == "" {
  297. newSubID := uuid.NewString()
  298. subNeedRestart, subErr := s.writeBackClientSubID(sourceInbound.Id, sourceClient, newSubID)
  299. if subErr != nil {
  300. result.Errors = append(result.Errors, fmt.Sprintf("%s: failed to write source subId: %v", originalEmail, subErr))
  301. continue
  302. }
  303. if subNeedRestart {
  304. needRestart = true
  305. }
  306. sourceClient.SubID = newSubID
  307. }
  308. targetEmail := s.nextAvailableCopiedEmail(originalEmail, targetInboundID, occupiedEmails)
  309. targetClient, buildErr := s.buildTargetClientFromSource(sourceClient, targetInbound, targetEmail, flow)
  310. if buildErr != nil {
  311. result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", originalEmail, buildErr))
  312. continue
  313. }
  314. newClients = append(newClients, targetClient)
  315. result.Added = append(result.Added, targetEmail)
  316. }
  317. if len(newClients) == 0 {
  318. return result, needRestart, nil
  319. }
  320. settingsPayload, err := json.Marshal(map[string][]model.Client{
  321. "clients": newClients,
  322. })
  323. if err != nil {
  324. return result, needRestart, err
  325. }
  326. addNeedRestart, err := s.clientService.AddInboundClient(s, &model.Inbound{
  327. Id: targetInboundID,
  328. Settings: string(settingsPayload),
  329. })
  330. if err != nil {
  331. return result, needRestart, err
  332. }
  333. if addNeedRestart {
  334. needRestart = true
  335. }
  336. return result, needRestart, nil
  337. }
  338. func (s *InboundService) GetClientInboundByTrafficID(trafficId int) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
  339. db := database.GetDB()
  340. var traffics []*xray.ClientTraffic
  341. err = db.Model(xray.ClientTraffic{}).Where("id = ?", trafficId).Find(&traffics).Error
  342. if err != nil {
  343. logger.Warningf("Error retrieving ClientTraffic with trafficId %d: %v", trafficId, err)
  344. return nil, nil, err
  345. }
  346. if len(traffics) == 0 {
  347. return nil, nil, nil
  348. }
  349. traffic = traffics[0]
  350. inbound, err = s.GetInbound(traffic.InboundId)
  351. if errors.Is(err, gorm.ErrRecordNotFound) {
  352. // client_traffics.inbound_id goes stale when an inbound is deleted and
  353. // recreated; fall back to the authoritative client_inbounds link by email.
  354. ids, idErr := s.clientService.GetInboundIdsForEmail(db, traffic.Email)
  355. if idErr != nil {
  356. return traffic, nil, idErr
  357. }
  358. if len(ids) > 0 {
  359. inbound, err = s.GetInbound(ids[0])
  360. }
  361. }
  362. return traffic, inbound, err
  363. }
  364. func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
  365. db := database.GetDB()
  366. var traffics []*xray.ClientTraffic
  367. err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
  368. if err != nil {
  369. logger.Warningf("Error retrieving ClientTraffic with email %s: %v", email, err)
  370. return nil, nil, err
  371. }
  372. if len(traffics) == 0 {
  373. return nil, nil, nil
  374. }
  375. traffic = traffics[0]
  376. inbound, err = s.GetInbound(traffic.InboundId)
  377. if errors.Is(err, gorm.ErrRecordNotFound) {
  378. // client_traffics.inbound_id is a legacy single-inbound pointer that goes
  379. // stale when an inbound is deleted and recreated: the email-keyed traffic
  380. // row survives but still references the missing inbound. Fall back to the
  381. // authoritative client_inbounds link so email lookups (reset, info, …) work.
  382. ids, idErr := s.clientService.GetInboundIdsForEmail(db, email)
  383. if idErr != nil {
  384. return traffic, nil, idErr
  385. }
  386. if len(ids) > 0 {
  387. inbound, err = s.GetInbound(ids[0])
  388. }
  389. }
  390. if err == nil && inbound != nil && !s.inboundHasClientEmail(inbound, email) {
  391. // The pointed-at inbound still exists but no longer carries the client —
  392. // the client was moved to another inbound (#6059). Resolve through the
  393. // client_inbounds link to the inbound that actually hosts it now.
  394. ids, idErr := s.clientService.GetInboundIdsForEmail(db, email)
  395. if idErr == nil {
  396. for _, id := range ids {
  397. if id == inbound.Id {
  398. continue
  399. }
  400. if other, oErr := s.GetInbound(id); oErr == nil && s.inboundHasClientEmail(other, email) {
  401. inbound = other
  402. break
  403. }
  404. }
  405. }
  406. }
  407. return traffic, inbound, err
  408. }
  409. func (s *InboundService) inboundHasClientEmail(inbound *model.Inbound, email string) bool {
  410. clients, err := s.GetClients(inbound)
  411. if err != nil {
  412. return false
  413. }
  414. for _, client := range clients {
  415. if client.Email == email {
  416. return true
  417. }
  418. }
  419. return false
  420. }
  421. func (s *InboundService) GetClientByEmail(clientEmail string) (*xray.ClientTraffic, *model.Client, error) {
  422. traffic, inbound, err := s.GetClientInboundByEmail(clientEmail)
  423. if err != nil {
  424. return nil, nil, err
  425. }
  426. if inbound == nil {
  427. return nil, nil, common.NewError("Inbound Not Found For Email:", clientEmail)
  428. }
  429. clients, err := s.GetClients(inbound)
  430. if err != nil {
  431. return nil, nil, err
  432. }
  433. for _, client := range clients {
  434. if client.Email == clientEmail {
  435. return traffic, &client, nil
  436. }
  437. }
  438. return nil, nil, common.NewError("Client Not Found In Inbound For Email:", clientEmail)
  439. }
  440. // EmailsByInbound returns the list of client emails currently configured on
  441. // an inbound's settings.clients[]. Used by the "delete all clients" flow on
  442. // the inbounds page, which then feeds the list into ClientService.BulkDelete.
  443. func (s *InboundService) EmailsByInbound(inboundId int) ([]string, error) {
  444. inbound, err := s.GetInbound(inboundId)
  445. if err != nil {
  446. return nil, err
  447. }
  448. clients, err := s.GetClients(inbound)
  449. if err != nil {
  450. return nil, err
  451. }
  452. emails := make([]string, 0, len(clients))
  453. for _, c := range clients {
  454. if e := strings.TrimSpace(c.Email); e != "" {
  455. emails = append(emails, e)
  456. }
  457. }
  458. return emails, nil
  459. }