client_bulk.go 54 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  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. // BulkAttachResult reports the outcome of a bulk attach across target inbounds.
  17. type BulkAttachResult struct {
  18. Attached []string `json:"attached"`
  19. Skipped []string `json:"skipped"`
  20. Errors []string `json:"errors"`
  21. }
  22. // BulkAttach attaches the given existing clients (by email) to each target inbound,
  23. // reusing their identity (email/UUID/password/subId) and a shared traffic row. It adds
  24. // all clients to a target in a single AddInboundClient call, and reports clients already
  25. // present on a target as skipped.
  26. func (s *ClientService) BulkAttach(inboundSvc *InboundService, emails []string, inboundIds []int) (*BulkAttachResult, bool, error) {
  27. result := &BulkAttachResult{}
  28. if len(emails) == 0 || len(inboundIds) == 0 {
  29. return result, false, nil
  30. }
  31. recordErr := func(format string, args ...any) {
  32. msg := fmt.Sprintf(format, args...)
  33. result.Errors = append(result.Errors, msg)
  34. logger.Warningf("[BulkAttach] %s", msg)
  35. }
  36. records := make([]*model.ClientRecord, 0, len(emails))
  37. seenEmail := make(map[string]struct{}, len(emails))
  38. for _, email := range emails {
  39. if email == "" {
  40. continue
  41. }
  42. key := strings.ToLower(email)
  43. if _, ok := seenEmail[key]; ok {
  44. continue
  45. }
  46. seenEmail[key] = struct{}{}
  47. rec, err := s.GetRecordByEmail(nil, email)
  48. if err != nil {
  49. recordErr("%s: %v", email, err)
  50. continue
  51. }
  52. records = append(records, rec)
  53. }
  54. // Same rule as Attach (#4834): clients.flow is unreliable when a non-flow
  55. // inbound synced last, so seed from EffectiveFlow before clientWithInboundFlow.
  56. emailsForFlow := make([]string, 0, len(records))
  57. for _, rec := range records {
  58. emailsForFlow = append(emailsForFlow, rec.Email)
  59. }
  60. flowsByEmail, err := s.EffectiveFlowsByEmails(nil, emailsForFlow)
  61. if err != nil {
  62. return result, false, err
  63. }
  64. needRestart := false
  65. // Prepared in order first, as in Create: fillProtocolDefaults mints the
  66. // shared credentials, so only the node pushes below may overlap.
  67. attachIds := make([]int, 0, len(inboundIds))
  68. attachPayloads := make([]string, 0, len(inboundIds))
  69. attachClients := make([][]model.Client, 0, len(inboundIds))
  70. attachAnyTunnel := false
  71. // A repeated id used to be caught by the second pass seeing the client
  72. // already attached; the applies no longer run before the next prep.
  73. seenInbound := make(map[int]struct{}, len(inboundIds))
  74. for _, ibId := range inboundIds {
  75. if _, dup := seenInbound[ibId]; dup {
  76. continue
  77. }
  78. seenInbound[ibId] = struct{}{}
  79. inbound, err := inboundSvc.GetInbound(ibId)
  80. if err != nil {
  81. recordErr("inbound %d: %v", ibId, err)
  82. continue
  83. }
  84. if inbound.Protocol == model.WireGuard || inbound.Protocol == model.AmneziaWG {
  85. attachAnyTunnel = true
  86. }
  87. existingClients, err := inboundSvc.GetClients(inbound)
  88. if err != nil {
  89. recordErr("inbound %d: %v", ibId, err)
  90. continue
  91. }
  92. have := make(map[string]struct{}, len(existingClients))
  93. for _, c := range existingClients {
  94. have[strings.ToLower(c.Email)] = struct{}{}
  95. }
  96. clientsToAdd := make([]model.Client, 0, len(records))
  97. for _, rec := range records {
  98. if _, attached := have[strings.ToLower(rec.Email)]; attached {
  99. result.Skipped = append(result.Skipped, rec.Email)
  100. continue
  101. }
  102. client := *rec.ToClient()
  103. if flow, ok := flowsByEmail[rec.Email]; ok && flow != "" {
  104. client.Flow = flow
  105. }
  106. client.UpdatedAt = time.Now().UnixMilli()
  107. if err := s.fillProtocolDefaults(&client, inbound); err != nil {
  108. recordErr("%s -> inbound %d: %v", rec.Email, ibId, err)
  109. continue
  110. }
  111. clientsToAdd = append(clientsToAdd, clientWithInboundFlow(client, inbound))
  112. }
  113. if len(clientsToAdd) == 0 {
  114. continue
  115. }
  116. payload, err := json.Marshal(map[string][]model.Client{"clients": clientsToAdd})
  117. if err != nil {
  118. recordErr("inbound %d: %v", ibId, err)
  119. continue
  120. }
  121. attachIds = append(attachIds, ibId)
  122. attachPayloads = append(attachPayloads, string(payload))
  123. attachClients = append(attachClients, clientsToAdd)
  124. }
  125. attachResults, attachPanics := fanoutInboundResults(attachIds, addFanoutLimit(attachAnyTunnel), func(i int) inboundApplyOutcome {
  126. nr, err := s.AddInboundClient(inboundSvc, &model.Inbound{Id: attachIds[i], Settings: attachPayloads[i]})
  127. return inboundApplyOutcome{needRestart: nr, err: err}
  128. })
  129. for i, out := range attachResults {
  130. err := out.err
  131. if attachPanics[i] != nil {
  132. // The apply may already have committed, so ask for the restart the
  133. // lost return value can no longer report.
  134. needRestart = true
  135. err = attachPanics[i]
  136. }
  137. if err != nil {
  138. recordErr("inbound %d: %v", attachIds[i], err)
  139. continue
  140. }
  141. if out.needRestart {
  142. needRestart = true
  143. }
  144. for _, c := range attachClients[i] {
  145. result.Attached = append(result.Attached, c.Email)
  146. }
  147. }
  148. return result, needRestart, nil
  149. }
  150. // BulkDetachResult reports the outcome of a bulk detach across target inbounds.
  151. type BulkDetachResult struct {
  152. Detached []string `json:"detached"`
  153. Skipped []string `json:"skipped"`
  154. Errors []string `json:"errors"`
  155. }
  156. // BulkDetach detaches the given existing clients (by email) from each target inbound.
  157. // (email, inbound) pairs where the client is not currently attached are silently skipped
  158. // at the inbound level; emails that aren't attached to any of the requested inbounds
  159. // are reported under skipped. ClientRecord rows are kept even when they become orphaned
  160. // (matches single-client detach semantics); callers should use bulkDelete for full removal.
  161. func (s *ClientService) BulkDetach(inboundSvc *InboundService, emails []string, inboundIds []int) (*BulkDetachResult, bool, error) {
  162. result := &BulkDetachResult{}
  163. if len(emails) == 0 || len(inboundIds) == 0 {
  164. return result, false, nil
  165. }
  166. recordErr := func(format string, args ...any) {
  167. msg := fmt.Sprintf(format, args...)
  168. result.Errors = append(result.Errors, msg)
  169. logger.Warningf("[BulkDetach] %s", msg)
  170. }
  171. requested := make(map[int]struct{}, len(inboundIds))
  172. for _, id := range inboundIds {
  173. requested[id] = struct{}{}
  174. }
  175. recsByInbound := make(map[int][]*model.ClientRecord)
  176. emailOrder := make([]string, 0, len(emails))
  177. emailRepr := make(map[string]string, len(emails))
  178. emailFailed := make(map[string]bool, len(emails))
  179. seenEmail := make(map[string]struct{}, len(emails))
  180. for _, email := range emails {
  181. if email == "" {
  182. continue
  183. }
  184. key := strings.ToLower(email)
  185. if _, ok := seenEmail[key]; ok {
  186. continue
  187. }
  188. seenEmail[key] = struct{}{}
  189. rec, err := s.GetRecordByEmail(nil, email)
  190. if err != nil {
  191. recordErr("%s: %v", email, err)
  192. continue
  193. }
  194. currentIds, err := s.GetInboundIdsForRecord(rec.Id)
  195. if err != nil {
  196. recordErr("%s: %v", email, err)
  197. continue
  198. }
  199. matched := false
  200. for _, id := range currentIds {
  201. if _, ok := requested[id]; ok {
  202. recsByInbound[id] = append(recsByInbound[id], rec)
  203. matched = true
  204. }
  205. }
  206. if !matched {
  207. result.Skipped = append(result.Skipped, rec.Email)
  208. continue
  209. }
  210. emailOrder = append(emailOrder, key)
  211. emailRepr[key] = rec.Email
  212. }
  213. needRestart := false
  214. // Ordered and de-duplicated up front: the sequential loop dropped each map
  215. // entry as it went, which the concurrent applies can no longer do.
  216. detachIds := make([]int, 0, len(recsByInbound))
  217. detachRecs := make([][]*model.ClientRecord, 0, len(recsByInbound))
  218. for _, ibId := range inboundIds {
  219. recs, ok := recsByInbound[ibId]
  220. if !ok {
  221. continue
  222. }
  223. delete(recsByInbound, ibId)
  224. detachIds = append(detachIds, ibId)
  225. detachRecs = append(detachRecs, recs)
  226. }
  227. detachResults, detachPanics := fanoutInboundResults(detachIds, inboundFanoutConcurrency, func(i int) inboundApplyOutcome {
  228. nr, err := s.delInboundClients(inboundSvc, detachIds[i], detachRecs[i], true)
  229. return inboundApplyOutcome{needRestart: nr, err: err}
  230. })
  231. for i, out := range detachResults {
  232. err := out.err
  233. if detachPanics[i] != nil {
  234. // See BulkAttach: a panicking apply may already have committed.
  235. needRestart = true
  236. err = detachPanics[i]
  237. }
  238. if err != nil {
  239. recordErr("inbound %d: %v", detachIds[i], err)
  240. for _, rec := range detachRecs[i] {
  241. emailFailed[strings.ToLower(rec.Email)] = true
  242. }
  243. continue
  244. }
  245. if out.needRestart {
  246. needRestart = true
  247. }
  248. }
  249. for _, key := range emailOrder {
  250. if emailFailed[key] {
  251. continue
  252. }
  253. result.Detached = append(result.Detached, emailRepr[key])
  254. }
  255. return result, needRestart, nil
  256. }
  257. // inboundApplyOutcome carries one inbound's apply result out of the fanout.
  258. type inboundApplyOutcome struct {
  259. needRestart bool
  260. err error
  261. }
  262. // BulkAdjustResult is returned by BulkAdjust to report how many clients were
  263. // successfully updated and which were skipped (typically because the field
  264. // being adjusted was unlimited for that client) or failed.
  265. type BulkAdjustResult struct {
  266. Adjusted int `json:"adjusted"`
  267. Skipped []BulkAdjustReport `json:"skipped,omitempty"`
  268. }
  269. type BulkAdjustReport struct {
  270. Email string `json:"email"`
  271. Reason string `json:"reason"`
  272. }
  273. type bulkAdjustEntry struct {
  274. record *model.ClientRecord
  275. applyExpiry bool
  276. newExpiry int64
  277. applyTotal bool
  278. newTotal int64
  279. }
  280. // bulkFlowClear is the directive that strips the XTLS flow from every selected
  281. // client. The vision values are the only positive flows xray accepts.
  282. const bulkFlowClear = "none"
  283. // bulkFlowAllowed whitelists the flow directives BulkAdjust accepts. Anything
  284. // outside this set is treated as "" (leave flow untouched) so a malformed or
  285. // hostile value can never be injected into a client's settings. The dropdown in
  286. // ClientBulkAdjustModal.tsx offers the same set ("" / "none" / TLS_FLOW_CONTROL);
  287. // keep the two in sync.
  288. var bulkFlowAllowed = map[string]struct{}{
  289. "": {},
  290. bulkFlowClear: {},
  291. "xtls-rprx-vision": {},
  292. "xtls-rprx-vision-udp443": {},
  293. }
  294. // BulkAdjust shifts ExpiryTime by addDays (days) and TotalGB by addBytes
  295. // for every email in the list. Clients whose corresponding field is
  296. // unlimited (0) are skipped — bulk extend should not accidentally
  297. // limit an unlimited client. addDays and addBytes may be negative.
  298. // flow sets the XTLS flow, limitHwid the max registered devices (0 = unlimited)
  299. // and adTag the MTProto sponsor channel; "none" clears flow or adTag.
  300. //
  301. // Like BulkDelete, the work is grouped by inbound so each inbound's
  302. // settings JSON is parsed and written exactly once regardless of how
  303. // many target emails it contains.
  304. func (s *ClientService) BulkAdjust(inboundSvc *InboundService, emails []string, addDays int, addBytes int64, flow string, limitHwid *int, adTag string) (BulkAdjustResult, bool, error) {
  305. result := BulkAdjustResult{}
  306. if len(emails) == 0 {
  307. return result, false, nil
  308. }
  309. flow = strings.TrimSpace(flow)
  310. if _, ok := bulkFlowAllowed[flow]; !ok {
  311. flow = "" // ignore unknown directives — "" means "leave flow untouched"
  312. }
  313. adTag = strings.TrimSpace(adTag)
  314. if adTag != "" && adTag != bulkFlowClear && !model.ValidMtprotoAdTag(adTag) {
  315. return result, false, common.NewError("mtproto client ad tag must be 32 hex characters")
  316. }
  317. if limitHwid != nil && *limitHwid < 0 {
  318. zero := 0
  319. limitHwid = &zero
  320. }
  321. adjustFlow := flow != ""
  322. adjustHwid := limitHwid != nil
  323. adjustAdTag := adTag != ""
  324. if addDays == 0 && addBytes == 0 && !adjustFlow && !adjustHwid && !adjustAdTag {
  325. return result, false, common.NewError("no adjustment specified")
  326. }
  327. addExpiryMs := int64(addDays) * 24 * 60 * 60 * 1000
  328. cleanEmails := trimmedUniqueEmails(emails)
  329. if len(cleanEmails) == 0 {
  330. return result, false, nil
  331. }
  332. db := database.GetDB()
  333. recordsByEmail, err := clientRecordsByEmail(db, cleanEmails)
  334. if err != nil {
  335. return result, false, err
  336. }
  337. skippedReasons := map[string]string{}
  338. for _, email := range cleanEmails {
  339. if _, ok := recordsByEmail[email]; !ok {
  340. skippedReasons[email] = "client not found"
  341. }
  342. }
  343. plan := map[string]*bulkAdjustEntry{}
  344. for email, rec := range recordsByEmail {
  345. entry := &bulkAdjustEntry{record: rec}
  346. if addDays != 0 {
  347. switch {
  348. case rec.ExpiryTime == 0:
  349. if _, exists := skippedReasons[email]; !exists {
  350. skippedReasons[email] = "unlimited expiry"
  351. }
  352. case rec.ExpiryTime > 0:
  353. next := rec.ExpiryTime + addExpiryMs
  354. if next <= 0 {
  355. if _, exists := skippedReasons[email]; !exists {
  356. skippedReasons[email] = "reduction exceeds remaining time"
  357. }
  358. } else {
  359. entry.applyExpiry = true
  360. entry.newExpiry = next
  361. }
  362. default:
  363. next := rec.ExpiryTime - addExpiryMs
  364. if next >= 0 {
  365. if _, exists := skippedReasons[email]; !exists {
  366. skippedReasons[email] = "reduction exceeds delay window"
  367. }
  368. } else {
  369. entry.applyExpiry = true
  370. entry.newExpiry = next
  371. }
  372. }
  373. }
  374. if addBytes != 0 {
  375. if rec.TotalGB == 0 {
  376. if _, exists := skippedReasons[email]; !exists {
  377. skippedReasons[email] = "unlimited traffic"
  378. }
  379. } else {
  380. next := rec.TotalGB + addBytes
  381. if next <= 0 {
  382. if _, exists := skippedReasons[email]; !exists {
  383. skippedReasons[email] = "reduction exceeds remaining quota"
  384. }
  385. } else {
  386. entry.applyTotal = true
  387. entry.newTotal = next
  388. }
  389. }
  390. }
  391. if entry.applyExpiry || entry.applyTotal || adjustFlow || adjustHwid || adjustAdTag {
  392. plan[email] = entry
  393. }
  394. }
  395. if len(plan) == 0 {
  396. for email, reason := range skippedReasons {
  397. result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: reason})
  398. }
  399. return result, false, nil
  400. }
  401. plannedIds := make([]int, 0, len(plan))
  402. recordIdToEmail := make(map[int]string, len(plan))
  403. for email, entry := range plan {
  404. if entry.applyExpiry || entry.applyTotal || adjustFlow || adjustAdTag {
  405. plannedIds = append(plannedIds, entry.record.Id)
  406. recordIdToEmail[entry.record.Id] = email
  407. }
  408. }
  409. var mappings []model.ClientInbound
  410. for _, batch := range chunkInts(plannedIds, sqlInChunk) {
  411. var rows []model.ClientInbound
  412. if err := db.Where("client_id IN ?", batch).Find(&rows).Error; err != nil {
  413. return result, false, err
  414. }
  415. mappings = append(mappings, rows...)
  416. }
  417. emailsByInbound := map[int][]string{}
  418. for _, m := range mappings {
  419. email, ok := recordIdToEmail[m.ClientId]
  420. if !ok {
  421. continue
  422. }
  423. emailsByInbound[m.InboundId] = append(emailsByInbound[m.InboundId], email)
  424. }
  425. needRestart := false
  426. flowHonored := map[string]bool{}
  427. flowIneligible := map[string]bool{}
  428. adTagHonored := map[string]bool{}
  429. adTagIneligible := map[string]bool{}
  430. execFailed := map[string]bool{}
  431. adjustIds := sortedInboundIds(emailsByInbound)
  432. adjustResults, adjustPanics := fanoutInboundResults(adjustIds, inboundFanoutConcurrency, func(i int) bulkInboundAdjustResult {
  433. return s.bulkAdjustInboundClients(inboundSvc, adjustIds[i], emailsByInbound[adjustIds[i]], plan, flow, adTag)
  434. })
  435. for i, ibRes := range adjustResults {
  436. if adjustPanics[i] != nil {
  437. needRestart = true
  438. for _, email := range emailsByInbound[adjustIds[i]] {
  439. execFailed[email] = true
  440. if _, already := skippedReasons[email]; !already {
  441. skippedReasons[email] = adjustPanics[i].Error()
  442. }
  443. }
  444. continue
  445. }
  446. if ibRes.needRestart {
  447. needRestart = true
  448. }
  449. for email := range ibRes.flowHonored {
  450. flowHonored[email] = true
  451. }
  452. for email := range ibRes.flowIneligible {
  453. flowIneligible[email] = true
  454. }
  455. for email := range ibRes.adTagHonored {
  456. adTagHonored[email] = true
  457. }
  458. for email := range ibRes.adTagIneligible {
  459. adTagIneligible[email] = true
  460. }
  461. for email, reason := range ibRes.perEmailSkipped {
  462. execFailed[email] = true
  463. if _, already := skippedReasons[email]; !already {
  464. skippedReasons[email] = reason
  465. }
  466. }
  467. }
  468. cond, condArgs := depletedCond(db)
  469. candidateEmails := make([]string, 0, len(plan))
  470. for email, entry := range plan {
  471. if entry.applyExpiry || entry.applyTotal {
  472. candidateEmails = append(candidateEmails, email)
  473. }
  474. }
  475. wasDisabledDepleted := map[string]struct{}{}
  476. for _, batch := range chunkStrings(candidateEmails, sqlInChunk) {
  477. var rows []string
  478. if err := db.Model(xray.ClientTraffic{}).
  479. Where(cond+" AND enable = ? AND email IN ?", append(append([]any{}, condArgs...), false, batch)...).
  480. Pluck("email", &rows).Error; err != nil {
  481. return result, needRestart, err
  482. }
  483. for _, e := range rows {
  484. wasDisabledDepleted[e] = struct{}{}
  485. }
  486. }
  487. wantAdTag := ""
  488. if adjustAdTag && adTag != bulkFlowClear {
  489. wantAdTag = strings.ToLower(adTag)
  490. }
  491. adjusted := map[string]struct{}{}
  492. for email, entry := range plan {
  493. if execFailed[email] {
  494. continue
  495. }
  496. updates := map[string]any{}
  497. if entry.applyExpiry {
  498. updates["expiry_time"] = entry.newExpiry
  499. }
  500. if entry.applyTotal {
  501. updates["total"] = entry.newTotal
  502. }
  503. if len(updates) > 0 {
  504. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).Updates(updates).Error; err != nil {
  505. if _, already := skippedReasons[email]; !already {
  506. skippedReasons[email] = err.Error()
  507. }
  508. continue
  509. }
  510. }
  511. if adjustHwid {
  512. if err := s.setClientLimitHwidByEmail(db, email, *limitHwid); err != nil {
  513. if _, already := skippedReasons[email]; !already {
  514. skippedReasons[email] = err.Error()
  515. }
  516. continue
  517. }
  518. }
  519. if adjustAdTag && adTagHonored[email] {
  520. if err := db.Model(&model.ClientRecord{}).Where("email = ?", email).UpdateColumn("ad_tag", wantAdTag).Error; err != nil {
  521. if _, already := skippedReasons[email]; !already {
  522. skippedReasons[email] = err.Error()
  523. }
  524. continue
  525. }
  526. }
  527. // Counted when expiry/total changed, flow was honored, adTag was honored, or limitHwid was adjusted.
  528. if len(updates) > 0 || flowHonored[email] || adTagHonored[email] || adjustHwid {
  529. adjusted[email] = struct{}{}
  530. }
  531. }
  532. result.Adjusted = len(adjusted)
  533. for email, reason := range skippedReasons {
  534. result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: reason})
  535. }
  536. // Report a flow directive that no inbound could carry — only when it was not
  537. // honored anywhere and the client has no other (expiry/total) skip reason.
  538. // The expiry/total part, if any, has already been applied and counted above.
  539. for email := range flowIneligible {
  540. if flowHonored[email] {
  541. continue
  542. }
  543. if _, already := skippedReasons[email]; already {
  544. continue
  545. }
  546. result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: "flow not supported on inbound"})
  547. }
  548. for email := range adTagIneligible {
  549. if adTagHonored[email] {
  550. continue
  551. }
  552. if _, already := skippedReasons[email]; already {
  553. continue
  554. }
  555. result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: "adTag not supported on inbound"})
  556. }
  557. if len(wasDisabledDepleted) > 0 {
  558. stillDepleted := map[string]struct{}{}
  559. wasList := make([]string, 0, len(wasDisabledDepleted))
  560. for e := range wasDisabledDepleted {
  561. wasList = append(wasList, e)
  562. }
  563. for _, batch := range chunkStrings(wasList, sqlInChunk) {
  564. var rows []string
  565. if err := db.Model(xray.ClientTraffic{}).
  566. Where(cond+" AND email IN ?", append(append([]any{}, condArgs...), batch)...).
  567. Pluck("email", &rows).Error; err != nil {
  568. return result, needRestart, err
  569. }
  570. for _, e := range rows {
  571. stillDepleted[e] = struct{}{}
  572. }
  573. }
  574. reEnable := make([]string, 0, len(wasDisabledDepleted))
  575. for e := range wasDisabledDepleted {
  576. if _, still := stillDepleted[e]; !still {
  577. reEnable = append(reEnable, e)
  578. }
  579. }
  580. if len(reEnable) > 0 {
  581. _, nr, err := s.BulkSetEnable(inboundSvc, reEnable, true)
  582. if err != nil {
  583. return result, needRestart, err
  584. }
  585. if nr {
  586. needRestart = true
  587. }
  588. }
  589. }
  590. return result, needRestart, nil
  591. }
  592. type bulkInboundAdjustResult struct {
  593. perEmailSkipped map[string]string
  594. flowHonored map[string]bool
  595. // flowIneligible is tracked apart from perEmailSkipped: a flow directive
  596. // that an inbound cannot carry must not suppress the expiry/total write for
  597. // the same client (which would diverge the inbound JSON / ClientRecord from
  598. // ClientTraffic). It only feeds the final Skipped report.
  599. flowIneligible map[string]bool
  600. adTagHonored map[string]bool
  601. adTagIneligible map[string]bool
  602. needRestart bool
  603. }
  604. // bulkAdjustInboundClients applies expiry/total deltas to multiple clients
  605. // inside a single inbound's settings JSON. The xray runtime is updated
  606. // only for remote-node inbounds; local nodes do not need a notification
  607. // because the AddUser payload does not include totalGB/expiryTime —
  608. // changing those fields is identity-preserving and the panel's traffic
  609. // enforcement loop picks up the new limits from ClientTraffic directly.
  610. func (s *ClientService) bulkAdjustInboundClients(
  611. inboundSvc *InboundService,
  612. inboundId int,
  613. emails []string,
  614. plan map[string]*bulkAdjustEntry,
  615. flow string,
  616. adTag string,
  617. ) bulkInboundAdjustResult {
  618. res := bulkInboundAdjustResult{
  619. perEmailSkipped: map[string]string{},
  620. flowHonored: map[string]bool{},
  621. flowIneligible: map[string]bool{},
  622. adTagHonored: map[string]bool{},
  623. adTagIneligible: map[string]bool{},
  624. }
  625. defer lockInbound(inboundId).Unlock()
  626. oldInbound, err := inboundSvc.GetInbound(inboundId)
  627. if err != nil {
  628. logger.Error("Load Old Data Error")
  629. for _, e := range emails {
  630. res.perEmailSkipped[e] = err.Error()
  631. }
  632. return res
  633. }
  634. var settings map[string]any
  635. if err := json.Unmarshal([]byte(oldInbound.Settings), &settings); err != nil {
  636. for _, e := range emails {
  637. res.perEmailSkipped[e] = err.Error()
  638. }
  639. return res
  640. }
  641. // Match by email — the client's stable identity (see Delete). Credentials
  642. // can drift from the inbound JSON, so they are never used for matching.
  643. wantedEmails := make(map[string]struct{}, len(emails))
  644. for _, email := range emails {
  645. if plan[email] == nil {
  646. res.perEmailSkipped[email] = "client not found"
  647. continue
  648. }
  649. wantedEmails[email] = struct{}{}
  650. }
  651. // Flow eligibility is a property of the inbound (protocol + transport), so
  652. // resolve it once. Clearing flow is always allowed; setting a vision flow
  653. // is only honored on an inbound that can carry it.
  654. flowEligible := flow == bulkFlowClear ||
  655. (!oldInbound.DisableFlow &&
  656. inboundCanEnableTlsFlow(string(oldInbound.Protocol), oldInbound.StreamSettings, oldInbound.Settings))
  657. wantAdTag := ""
  658. if adTag != "" && adTag != bulkFlowClear {
  659. wantAdTag = strings.ToLower(adTag)
  660. }
  661. interfaceClients, _ := settings["clients"].([]any)
  662. foundEmails := map[string]bool{}
  663. flowChanged := false
  664. adTagChanged := false
  665. hasInboundChanges := false
  666. nowMs := time.Now().Unix() * 1000
  667. for i, client := range interfaceClients {
  668. c, ok := client.(map[string]any)
  669. if !ok {
  670. continue
  671. }
  672. targetEmail, _ := c["email"].(string)
  673. if _, want := wantedEmails[targetEmail]; !want || targetEmail == "" {
  674. continue
  675. }
  676. clientChanged := false
  677. entry := plan[targetEmail]
  678. if entry.applyExpiry {
  679. c["expiryTime"] = entry.newExpiry
  680. clientChanged = true
  681. }
  682. if entry.applyTotal {
  683. c["totalGB"] = entry.newTotal
  684. clientChanged = true
  685. }
  686. if flow != "" {
  687. if flowEligible {
  688. want := ""
  689. if flow != bulkFlowClear {
  690. want = flow
  691. }
  692. if cur, _ := c["flow"].(string); cur != want {
  693. c["flow"] = want
  694. flowChanged = true
  695. }
  696. res.flowHonored[targetEmail] = true
  697. clientChanged = true
  698. } else {
  699. // Record separately so this never suppresses the expiry/total
  700. // write for the same client (see flowIneligible doc).
  701. res.flowIneligible[targetEmail] = true
  702. }
  703. }
  704. if adTag != "" {
  705. if oldInbound.Protocol == model.MTProto {
  706. if cur, _ := c["adTag"].(string); cur != wantAdTag {
  707. c["adTag"] = wantAdTag
  708. adTagChanged = true
  709. }
  710. res.adTagHonored[targetEmail] = true
  711. clientChanged = true
  712. } else {
  713. res.adTagIneligible[targetEmail] = true
  714. }
  715. }
  716. if clientChanged {
  717. c["updated_at"] = nowMs
  718. hasInboundChanges = true
  719. }
  720. interfaceClients[i] = c
  721. foundEmails[targetEmail] = true
  722. }
  723. for email := range wantedEmails {
  724. if !foundEmails[email] {
  725. res.perEmailSkipped[email] = "Client Not Found In Inbound"
  726. }
  727. }
  728. if len(foundEmails) == 0 || !hasInboundChanges {
  729. return res
  730. }
  731. settings["clients"] = interfaceClients
  732. newSettings, err := json.MarshalIndent(settings, "", " ")
  733. if err != nil {
  734. for email := range foundEmails {
  735. res.perEmailSkipped[email] = err.Error()
  736. }
  737. return res
  738. }
  739. oldInbound.Settings = string(newSettings)
  740. // A flow change rewrites the user's xray config, which the lightweight
  741. // UpdateUser push below does not carry. Local nodes reload via restart;
  742. // remote nodes get a full reconcile (MarkNodeDirty) instead of a per-user push.
  743. if flowChanged && oldInbound.NodeID == nil {
  744. res.needRestart = true
  745. }
  746. // Serialize against the traffic poll to avoid the cross-transaction
  747. // lock-order deadlock on inbounds/client_records (runSerializedTx).
  748. txErr := runSerializedTx(func(tx *gorm.DB) error {
  749. if err := tx.Save(oldInbound).Error; err != nil {
  750. return err
  751. }
  752. finalClients, gcErr := inboundSvc.GetClients(oldInbound)
  753. if gcErr != nil {
  754. return gcErr
  755. }
  756. if err := s.SyncInbound(tx, inboundId, finalClients); err != nil {
  757. return err
  758. }
  759. if oldInbound.NodeID != nil {
  760. return (&NodeService{}).MarkNodeDirtyTx(tx, *oldInbound.NodeID)
  761. }
  762. return nil
  763. })
  764. if txErr != nil {
  765. for email := range foundEmails {
  766. if _, skip := res.perEmailSkipped[email]; !skip {
  767. res.perEmailSkipped[email] = txErr.Error()
  768. }
  769. }
  770. } else {
  771. if adTagChanged && oldInbound.Protocol == model.MTProto && oldInbound.NodeID == nil {
  772. inboundSvc.applyLocalMtproto(oldInbound.Id)
  773. }
  774. if oldInbound.NodeID != nil && !flowChanged && len(foundEmails) <= nodeBulkPushThreshold {
  775. rt, push, _, perr := inboundSvc.nodePushPlan(oldInbound)
  776. if perr != nil {
  777. logger.Warning("BulkAdjust: node runtime lookup after commit failed:", perr)
  778. } else if push {
  779. for email := range foundEmails {
  780. entry := plan[email]
  781. updated := *entry.record.ToClient()
  782. if entry.applyExpiry {
  783. updated.ExpiryTime = entry.newExpiry
  784. }
  785. if entry.applyTotal {
  786. updated.TotalGB = entry.newTotal
  787. }
  788. if adTag != "" && oldInbound.Protocol == model.MTProto {
  789. updated.AdTag = wantAdTag
  790. }
  791. updated.UpdatedAt = nowMs
  792. ctx, cancel := nodePushContext()
  793. err1 := rt.UpdateUser(ctx, oldInbound, email, updated)
  794. cancel()
  795. if err1 != nil {
  796. logger.Warning("Error in updating client on", rt.Name(), ":", err1)
  797. // First failure ends the batch push; the reconcile converges the rest.
  798. break
  799. }
  800. }
  801. }
  802. }
  803. }
  804. return res
  805. }
  806. // BulkDeleteResult mirrors BulkAdjustResult: total deleted plus per-email
  807. // skip reasons when an email could not be processed.
  808. type BulkDeleteResult struct {
  809. Deleted int `json:"deleted"`
  810. Skipped []BulkDeleteReport `json:"skipped,omitempty"`
  811. }
  812. type BulkDeleteReport struct {
  813. Email string `json:"email"`
  814. Reason string `json:"reason"`
  815. }
  816. // BulkDelete removes every client in the list in one optimized pass.
  817. // Instead of running the full single-delete pipeline N times (which would
  818. // re-read, re-parse, and re-write each inbound's settings JSON for every
  819. // email), it groups emails by inbound and performs a single
  820. // read-modify-write per inbound. Per-row DB cleanups are also batched with
  821. // IN-clause queries at the end. Errors on a particular email are recorded
  822. // in the Skipped list and processing continues for the rest.
  823. func (s *ClientService) BulkDelete(inboundSvc *InboundService, emails []string, keepTraffic bool) (BulkDeleteResult, bool, error) {
  824. result := BulkDeleteResult{}
  825. cleanEmails := trimmedUniqueEmails(emails)
  826. if len(cleanEmails) == 0 {
  827. return result, false, nil
  828. }
  829. db := database.GetDB()
  830. recordsByEmail, err := clientRecordsByEmail(db, cleanEmails)
  831. if err != nil {
  832. return result, false, err
  833. }
  834. tombstoneEmails := make([]string, 0, len(recordsByEmail))
  835. for _, email := range cleanEmails {
  836. if recordsByEmail[email] != nil {
  837. tombstoneEmails = append(tombstoneEmails, email)
  838. }
  839. }
  840. tombstoneClientEmails(tombstoneEmails)
  841. skippedReasons := map[string]string{}
  842. for _, email := range cleanEmails {
  843. if _, ok := recordsByEmail[email]; !ok {
  844. skippedReasons[email] = "client not found"
  845. }
  846. }
  847. clientIds := make([]int, 0, len(recordsByEmail))
  848. recordIdToEmail := make(map[int]string, len(recordsByEmail))
  849. for _, r := range recordsByEmail {
  850. clientIds = append(clientIds, r.Id)
  851. recordIdToEmail[r.Id] = r.Email
  852. }
  853. emailsByInbound := map[int][]string{}
  854. if len(clientIds) > 0 {
  855. var mappings []model.ClientInbound
  856. for _, batch := range chunkInts(clientIds, sqlInChunk) {
  857. var rows []model.ClientInbound
  858. if err := db.Where("client_id IN ?", batch).Find(&rows).Error; err != nil {
  859. return result, false, err
  860. }
  861. mappings = append(mappings, rows...)
  862. }
  863. for _, m := range mappings {
  864. email, ok := recordIdToEmail[m.ClientId]
  865. if !ok {
  866. continue
  867. }
  868. emailsByInbound[m.InboundId] = append(emailsByInbound[m.InboundId], email)
  869. }
  870. }
  871. needRestart := false
  872. delIds := sortedInboundIds(emailsByInbound)
  873. delResults, delPanics := fanoutInboundResults(delIds, inboundFanoutConcurrency, func(i int) bulkInboundDeleteResult {
  874. return s.bulkDelInboundClients(inboundSvc, delIds[i], emailsByInbound[delIds[i]], recordsByEmail, keepTraffic)
  875. })
  876. for i, ibResult := range delResults {
  877. if delPanics[i] != nil {
  878. needRestart = true
  879. for _, email := range emailsByInbound[delIds[i]] {
  880. if _, already := skippedReasons[email]; !already {
  881. skippedReasons[email] = delPanics[i].Error()
  882. }
  883. }
  884. continue
  885. }
  886. if ibResult.needRestart {
  887. needRestart = true
  888. }
  889. for email, reason := range ibResult.perEmailSkipped {
  890. if _, already := skippedReasons[email]; !already {
  891. skippedReasons[email] = reason
  892. }
  893. }
  894. }
  895. successEmails := make([]string, 0, len(recordsByEmail))
  896. successIds := make([]int, 0, len(recordsByEmail))
  897. failedEmails := make([]string, 0, len(recordsByEmail))
  898. successSubIDs := make([]string, 0, len(recordsByEmail))
  899. for email, rec := range recordsByEmail {
  900. if _, skipped := skippedReasons[email]; skipped {
  901. failedEmails = append(failedEmails, email)
  902. continue
  903. }
  904. successEmails = append(successEmails, email)
  905. successIds = append(successIds, rec.Id)
  906. successSubIDs = append(successSubIDs, rec.SubID)
  907. }
  908. withdrawClientTombstones(failedEmails...)
  909. if len(successIds) > 0 {
  910. // Serialize the row cleanup against the traffic poll to avoid the
  911. // cross-transaction lock-order deadlock on client_traffics/inbounds.
  912. if err := runSerializedTx(func(tx *gorm.DB) error {
  913. if e := adjustGroupBaselinesForRemovedTraffic(tx, successEmails); e != nil {
  914. return e
  915. }
  916. if e := clearClientHwidsBySubIDTx(tx, successSubIDs...); e != nil {
  917. return e
  918. }
  919. for _, batch := range chunkInts(successIds, sqlInChunk) {
  920. if e := tx.Where("client_id IN ?", batch).Delete(&model.ClientInbound{}).Error; e != nil {
  921. return e
  922. }
  923. if e := tx.Where("client_id IN ?", batch).Delete(&model.ClientExternalLink{}).Error; e != nil {
  924. return e
  925. }
  926. }
  927. if !keepTraffic && len(successEmails) > 0 {
  928. for _, batch := range chunkStrings(successEmails, sqlInChunk) {
  929. if e := tx.Where("email IN ?", batch).Delete(&xray.ClientTraffic{}).Error; e != nil {
  930. return e
  931. }
  932. if e := tx.Where("client_email IN ?", batch).Delete(&model.InboundClientIps{}).Error; e != nil {
  933. return e
  934. }
  935. }
  936. }
  937. for _, batch := range chunkInts(successIds, sqlInChunk) {
  938. if e := tx.Where("id IN ?", batch).Delete(&model.ClientRecord{}).Error; e != nil {
  939. return e
  940. }
  941. }
  942. return nil
  943. }); err != nil {
  944. withdrawClientTombstones(successEmails...)
  945. return result, needRestart, err
  946. }
  947. }
  948. result.Deleted = len(successEmails)
  949. for email, reason := range skippedReasons {
  950. result.Skipped = append(result.Skipped, BulkDeleteReport{Email: email, Reason: reason})
  951. }
  952. return result, needRestart, nil
  953. }
  954. type bulkInboundDeleteResult struct {
  955. perEmailSkipped map[string]string
  956. needRestart bool
  957. }
  958. // bulkDelInboundClients removes multiple clients from a single inbound's
  959. // settings JSON in one read-modify-write cycle, runs the xray runtime
  960. // RemoveUser/DeleteUser calls, and persists the inbound. The returned map
  961. // holds per-email failure reasons; emails not present in the map are
  962. // considered successful for this inbound.
  963. func (s *ClientService) bulkDelInboundClients(
  964. inboundSvc *InboundService,
  965. inboundId int,
  966. emails []string,
  967. records map[string]*model.ClientRecord,
  968. keepTraffic bool,
  969. ) bulkInboundDeleteResult {
  970. res := bulkInboundDeleteResult{perEmailSkipped: map[string]string{}}
  971. defer lockInbound(inboundId).Unlock()
  972. oldInbound, err := inboundSvc.GetInbound(inboundId)
  973. if err != nil {
  974. logger.Error("Load Old Data Error")
  975. for _, e := range emails {
  976. res.perEmailSkipped[e] = err.Error()
  977. }
  978. return res
  979. }
  980. var settings map[string]any
  981. if err := json.Unmarshal([]byte(oldInbound.Settings), &settings); err != nil {
  982. for _, e := range emails {
  983. res.perEmailSkipped[e] = err.Error()
  984. }
  985. return res
  986. }
  987. // Match by email — the client's stable identity (see Delete). The link-derived
  988. // set is deletion intent: an email already absent from settings is successful,
  989. // while foundEmails tracks entries that still need settings-specific cleanup.
  990. wantedEmails := make(map[string]struct{}, len(emails))
  991. for _, email := range emails {
  992. if records[email] == nil {
  993. res.perEmailSkipped[email] = "client not found"
  994. continue
  995. }
  996. wantedEmails[email] = struct{}{}
  997. }
  998. interfaceClients, _ := settings["clients"].([]any)
  999. newClients := make([]any, 0, len(interfaceClients))
  1000. foundEmails := map[string]bool{}
  1001. enableByEmail := map[string]bool{}
  1002. for _, client := range interfaceClients {
  1003. c, ok := client.(map[string]any)
  1004. if !ok {
  1005. newClients = append(newClients, client)
  1006. continue
  1007. }
  1008. em, _ := c["email"].(string)
  1009. if _, found := wantedEmails[em]; found && em != "" {
  1010. foundEmails[em] = true
  1011. en, _ := c["enable"].(bool)
  1012. enableByEmail[em] = en
  1013. continue
  1014. }
  1015. newClients = append(newClients, client)
  1016. }
  1017. db := database.GetDB()
  1018. newClients = compactOrphans(db, newClients)
  1019. if newClients == nil {
  1020. newClients = []any{}
  1021. }
  1022. settings["clients"] = newClients
  1023. newSettings, err := json.MarshalIndent(settings, "", " ")
  1024. if err != nil {
  1025. for email := range wantedEmails {
  1026. if _, skip := res.perEmailSkipped[email]; !skip {
  1027. res.perEmailSkipped[email] = err.Error()
  1028. }
  1029. }
  1030. return res
  1031. }
  1032. oldInbound.Settings = string(newSettings)
  1033. foundList := make([]string, 0, len(foundEmails))
  1034. for email := range foundEmails {
  1035. foundList = append(foundList, email)
  1036. }
  1037. notDepletedByEmail := map[string]bool{}
  1038. if len(foundList) > 0 {
  1039. type trafficRow struct {
  1040. Email string
  1041. Enable bool
  1042. }
  1043. for _, batch := range chunkStrings(foundList, sqlInChunk) {
  1044. var rows []trafficRow
  1045. if err := db.Model(xray.ClientTraffic{}).
  1046. Where("email IN ?", batch).
  1047. Select("email, enable").
  1048. Scan(&rows).Error; err == nil {
  1049. for _, r := range rows {
  1050. notDepletedByEmail[r.Email] = r.Enable
  1051. }
  1052. }
  1053. }
  1054. }
  1055. var sharedSet map[string]bool
  1056. if !keepTraffic {
  1057. var sharedErr error
  1058. sharedSet, sharedErr = inboundSvc.emailsUsedByOtherInbounds(foundList, inboundId)
  1059. if sharedErr != nil {
  1060. for email := range wantedEmails {
  1061. res.perEmailSkipped[email] = sharedErr.Error()
  1062. }
  1063. return res
  1064. }
  1065. }
  1066. if !keepTraffic {
  1067. purge := make([]string, 0, len(foundEmails))
  1068. for email := range foundEmails {
  1069. if !sharedSet[strings.ToLower(strings.TrimSpace(email))] {
  1070. purge = append(purge, email)
  1071. }
  1072. }
  1073. if len(purge) > 0 {
  1074. // Serialize the IP/stat purge against the traffic poll to avoid the
  1075. // cross-transaction lock-order deadlock on client_traffics.
  1076. if delErr := runSerializedTx(func(tx *gorm.DB) error {
  1077. if e := inboundSvc.delClientIPsByEmails(tx, purge); e != nil {
  1078. logger.Error("Error in delete client IPs")
  1079. return e
  1080. }
  1081. if e := inboundSvc.delClientStatsByEmails(tx, purge); e != nil {
  1082. logger.Error("Delete stats Data Error")
  1083. return e
  1084. }
  1085. return nil
  1086. }); delErr != nil {
  1087. for _, email := range purge {
  1088. res.perEmailSkipped[email] = delErr.Error()
  1089. delete(foundEmails, email)
  1090. }
  1091. }
  1092. }
  1093. }
  1094. // Serialize against the traffic poll to avoid the cross-transaction
  1095. // lock-order deadlock on inbounds/client_records (runSerializedTx).
  1096. txErr := runSerializedTx(func(tx *gorm.DB) error {
  1097. if err := tx.Save(oldInbound).Error; err != nil {
  1098. return err
  1099. }
  1100. finalClients, err := inboundSvc.GetClients(oldInbound)
  1101. if err != nil {
  1102. return err
  1103. }
  1104. if err := s.SyncInbound(tx, inboundId, finalClients); err != nil {
  1105. return err
  1106. }
  1107. if oldInbound.NodeID != nil {
  1108. return (&NodeService{}).MarkNodeDirtyTx(tx, *oldInbound.NodeID)
  1109. }
  1110. return nil
  1111. })
  1112. if txErr != nil {
  1113. for email := range wantedEmails {
  1114. if _, skip := res.perEmailSkipped[email]; !skip {
  1115. res.perEmailSkipped[email] = txErr.Error()
  1116. }
  1117. }
  1118. } else if oldInbound.NodeID == nil {
  1119. rt, rterr := inboundSvc.runtimeFor(oldInbound)
  1120. if rterr != nil {
  1121. res.needRestart = true
  1122. } else {
  1123. for email := range foundEmails {
  1124. if !enableByEmail[email] || !notDepletedByEmail[email] {
  1125. continue
  1126. }
  1127. err1 := rt.RemoveUser(context.Background(), oldInbound, email)
  1128. if err1 == nil {
  1129. logger.Debug("Client deleted on", rt.Name(), ":", email)
  1130. } else if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", email)) {
  1131. logger.Debug("User is already deleted. Nothing to do more...")
  1132. } else {
  1133. logger.Debug("Error in deleting client on", rt.Name(), ":", err1)
  1134. res.needRestart = true
  1135. }
  1136. }
  1137. }
  1138. } else {
  1139. dispatchEmails := make([]string, 0, len(wantedEmails))
  1140. for email := range wantedEmails {
  1141. if _, skip := res.perEmailSkipped[email]; !skip {
  1142. dispatchEmails = append(dispatchEmails, email)
  1143. }
  1144. }
  1145. if len(dispatchEmails) > nodeBulkPushThreshold {
  1146. return res
  1147. }
  1148. rt, push, _, perr := inboundSvc.nodePushPlan(oldInbound)
  1149. if perr != nil {
  1150. logger.Warning("BulkDelete: node runtime lookup after commit failed:", perr)
  1151. } else if push {
  1152. for _, email := range dispatchEmails {
  1153. ctx, cancel := nodePushContext()
  1154. err1 := rt.DeleteClient(ctx, email)
  1155. cancel()
  1156. if err1 != nil {
  1157. logger.Warning("Error in deleting client on", rt.Name(), ":", err1)
  1158. // The node is already dirty, so one reconcile converges the rest of
  1159. // the batch instead of paying another deadline per client.
  1160. break
  1161. }
  1162. }
  1163. }
  1164. }
  1165. return res
  1166. }
  1167. // BulkCreateResult mirrors BulkAdjustResult for the create flow.
  1168. type BulkCreateResult struct {
  1169. Created int `json:"created"`
  1170. Skipped []BulkCreateReport `json:"skipped,omitempty"`
  1171. }
  1172. type BulkCreateReport struct {
  1173. Email string `json:"email"`
  1174. Reason string `json:"reason"`
  1175. }
  1176. func (s *ClientService) BulkCreate(inboundSvc *InboundService, payloads []ClientCreatePayload) (BulkCreateResult, bool, error) {
  1177. result := BulkCreateResult{}
  1178. if len(payloads) == 0 {
  1179. return result, false, nil
  1180. }
  1181. skip := func(email, reason string) {
  1182. if strings.TrimSpace(email) == "" {
  1183. email = "(missing email)"
  1184. }
  1185. result.Skipped = append(result.Skipped, BulkCreateReport{Email: email, Reason: reason})
  1186. }
  1187. type prepared struct {
  1188. client model.Client
  1189. inboundIds []int
  1190. limitHwid int
  1191. }
  1192. prep := make([]prepared, 0, len(payloads))
  1193. emails := make([]string, 0, len(payloads))
  1194. subIDs := make([]string, 0, len(payloads))
  1195. seenEmail := make(map[string]struct{}, len(payloads))
  1196. seenSubID := make(map[string]string, len(payloads))
  1197. for i := range payloads {
  1198. client := payloads[i].Client
  1199. email := strings.TrimSpace(client.Email)
  1200. if email == "" {
  1201. skip("", "client email is required")
  1202. continue
  1203. }
  1204. if verr := validateClientEmail(email); verr != nil {
  1205. skip(email, verr.Error())
  1206. continue
  1207. }
  1208. if verr := validateClientSubID(client.SubID); verr != nil {
  1209. skip(email, verr.Error())
  1210. continue
  1211. }
  1212. if verr := validateClientResetDay(client.ResetDay); verr != nil {
  1213. skip(email, verr.Error())
  1214. continue
  1215. }
  1216. if verr := validateClientResetMax(client.ResetMax); verr != nil {
  1217. skip(email, verr.Error())
  1218. continue
  1219. }
  1220. if verr := validateClientTrafficReset(client.TrafficReset, client.TrafficResetDay); verr != nil {
  1221. skip(email, verr.Error())
  1222. continue
  1223. }
  1224. if len(payloads[i].InboundIds) == 0 {
  1225. skip(email, "at least one inbound is required")
  1226. continue
  1227. }
  1228. client.Email = email
  1229. if client.SubID == "" {
  1230. client.SubID = uuid.NewString()
  1231. }
  1232. // Preserve enable (omit→true in UnmarshalJSON; explicit false kept) (#6478).
  1233. now := time.Now().UnixMilli()
  1234. if client.CreatedAt == 0 {
  1235. client.CreatedAt = now
  1236. }
  1237. client.UpdatedAt = now
  1238. le := strings.ToLower(email)
  1239. if _, dup := seenEmail[le]; dup {
  1240. skip(email, "email already in use: "+email)
  1241. continue
  1242. }
  1243. if owner, ok := seenSubID[client.SubID]; ok && owner != le {
  1244. skip(email, "subId already in use: "+client.SubID)
  1245. continue
  1246. }
  1247. seenEmail[le] = struct{}{}
  1248. seenSubID[client.SubID] = le
  1249. prep = append(prep, prepared{client: client, inboundIds: payloads[i].InboundIds, limitHwid: payloads[i].LimitHwid})
  1250. emails = append(emails, email)
  1251. subIDs = append(subIDs, client.SubID)
  1252. }
  1253. if len(prep) == 0 {
  1254. return result, false, nil
  1255. }
  1256. db := database.GetDB()
  1257. const lookupChunk = 400
  1258. existingByEmail := make(map[string]model.ClientRecord, len(emails))
  1259. for start := 0; start < len(emails); start += lookupChunk {
  1260. end := min(start+lookupChunk, len(emails))
  1261. var rows []model.ClientRecord
  1262. if e := db.Where("email IN ?", emails[start:end]).Find(&rows).Error; e != nil {
  1263. return result, false, e
  1264. }
  1265. for i := range rows {
  1266. existingByEmail[strings.ToLower(rows[i].Email)] = rows[i]
  1267. }
  1268. }
  1269. existingSubOwner := make(map[string]string, len(subIDs))
  1270. for start := 0; start < len(subIDs); start += lookupChunk {
  1271. end := min(start+lookupChunk, len(subIDs))
  1272. var rows []model.ClientRecord
  1273. if e := db.Where("sub_id IN ?", subIDs[start:end]).Find(&rows).Error; e != nil {
  1274. return result, false, e
  1275. }
  1276. for i := range rows {
  1277. existingSubOwner[rows[i].SubID] = strings.ToLower(rows[i].Email)
  1278. }
  1279. }
  1280. inboundCache := make(map[int]*model.Inbound)
  1281. getIb := func(id int) (*model.Inbound, error) {
  1282. if ib, ok := inboundCache[id]; ok {
  1283. return ib, nil
  1284. }
  1285. ib, e := inboundSvc.GetInbound(id)
  1286. if e != nil {
  1287. return nil, e
  1288. }
  1289. inboundCache[id] = ib
  1290. return ib, nil
  1291. }
  1292. byInbound := make(map[int][]model.Client)
  1293. idxByInbound := make(map[int][]int)
  1294. inboundOrder := make([]int, 0)
  1295. failed := make([]bool, len(prep))
  1296. reason := make([]string, len(prep))
  1297. createAnyTunnel := false
  1298. for idx := range prep {
  1299. le := strings.ToLower(prep[idx].client.Email)
  1300. if rec, ok := existingByEmail[le]; ok {
  1301. if rec.SubID != prep[idx].client.SubID {
  1302. failed[idx] = true
  1303. reason[idx] = "email already in use: " + prep[idx].client.Email
  1304. continue
  1305. }
  1306. if prep[idx].client.ID == "" {
  1307. prep[idx].client.ID = rec.UUID
  1308. }
  1309. if prep[idx].client.Password == "" {
  1310. prep[idx].client.Password = rec.Password
  1311. }
  1312. if prep[idx].client.Auth == "" {
  1313. prep[idx].client.Auth = rec.Auth
  1314. }
  1315. if prep[idx].client.Secret == "" {
  1316. prep[idx].client.Secret = rec.Secret
  1317. }
  1318. }
  1319. if owner, ok := existingSubOwner[prep[idx].client.SubID]; ok && owner != le {
  1320. failed[idx] = true
  1321. reason[idx] = "subId already in use: " + prep[idx].client.SubID
  1322. continue
  1323. }
  1324. ok := true
  1325. for _, ibId := range prep[idx].inboundIds {
  1326. ib, e := getIb(ibId)
  1327. if e != nil {
  1328. failed[idx] = true
  1329. reason[idx] = e.Error()
  1330. ok = false
  1331. break
  1332. }
  1333. if ib.Protocol == model.WireGuard || ib.Protocol == model.AmneziaWG {
  1334. createAnyTunnel = true
  1335. }
  1336. if e := s.fillProtocolDefaults(&prep[idx].client, ib); e != nil {
  1337. failed[idx] = true
  1338. reason[idx] = e.Error()
  1339. ok = false
  1340. break
  1341. }
  1342. }
  1343. if !ok {
  1344. continue
  1345. }
  1346. for _, ibId := range prep[idx].inboundIds {
  1347. ib, _ := getIb(ibId)
  1348. if _, seen := byInbound[ibId]; !seen {
  1349. inboundOrder = append(inboundOrder, ibId)
  1350. }
  1351. byInbound[ibId] = append(byInbound[ibId], clientWithInboundFlow(prep[idx].client, ib))
  1352. idxByInbound[ibId] = append(idxByInbound[ibId], idx)
  1353. }
  1354. }
  1355. needRestart := false
  1356. createResults, createPanics := fanoutInboundResults(inboundOrder, addFanoutLimit(createAnyTunnel), func(i int) inboundApplyOutcome {
  1357. ibId := inboundOrder[i]
  1358. payload, e := json.Marshal(map[string][]model.Client{"clients": byInbound[ibId]})
  1359. if e != nil {
  1360. return inboundApplyOutcome{err: e}
  1361. }
  1362. nr, e := s.AddInboundClient(inboundSvc, &model.Inbound{Id: ibId, Settings: string(payload)})
  1363. return inboundApplyOutcome{needRestart: nr, err: e}
  1364. })
  1365. for i, out := range createResults {
  1366. e := out.err
  1367. if createPanics[i] != nil {
  1368. // See BulkAttach: a panicking apply may already have committed.
  1369. needRestart = true
  1370. e = createPanics[i]
  1371. }
  1372. if e != nil {
  1373. for _, idx := range idxByInbound[inboundOrder[i]] {
  1374. failed[idx] = true
  1375. if reason[idx] == "" {
  1376. reason[idx] = e.Error()
  1377. }
  1378. }
  1379. continue
  1380. }
  1381. if out.needRestart {
  1382. needRestart = true
  1383. }
  1384. }
  1385. createdEmails := make([]string, 0, len(prep))
  1386. for idx := range prep {
  1387. if failed[idx] {
  1388. skip(prep[idx].client.Email, reason[idx])
  1389. continue
  1390. }
  1391. if err := s.setClientLimitHwidByEmail(nil, prep[idx].client.Email, prep[idx].limitHwid); err != nil {
  1392. skip(prep[idx].client.Email, err.Error())
  1393. continue
  1394. }
  1395. createdEmails = append(createdEmails, prep[idx].client.Email)
  1396. result.Created++
  1397. }
  1398. // A re-created email is a live identity again: a delete tombstone left
  1399. // standing makes the next node merge prune the new client's inbound links.
  1400. withdrawClientTombstones(createdEmails...)
  1401. return result, needRestart, nil
  1402. }
  1403. func (s *ClientService) DelDepleted(inboundSvc *InboundService) (int, bool, error) {
  1404. db := database.GetDB()
  1405. now := time.Now().UnixMilli()
  1406. depletedClause := depletedClientsClause
  1407. var rows []xray.ClientTraffic
  1408. if err := db.Where(depletedClause, now).Find(&rows).Error; err != nil {
  1409. return 0, false, err
  1410. }
  1411. if len(rows) == 0 {
  1412. return 0, false, nil
  1413. }
  1414. seen := make(map[string]struct{}, len(rows))
  1415. emails := make([]string, 0, len(rows))
  1416. for _, r := range rows {
  1417. if r.Email == "" {
  1418. continue
  1419. }
  1420. if _, ok := seen[r.Email]; ok {
  1421. continue
  1422. }
  1423. seen[r.Email] = struct{}{}
  1424. emails = append(emails, r.Email)
  1425. }
  1426. if len(emails) == 0 {
  1427. return 0, false, nil
  1428. }
  1429. res, needRestart, err := s.BulkDelete(inboundSvc, emails, false)
  1430. if err != nil {
  1431. return res.Deleted, needRestart, err
  1432. }
  1433. return res.Deleted, needRestart, nil
  1434. }
  1435. type BulkSetEnableResult struct {
  1436. Changed int `json:"changed"`
  1437. Skipped []BulkSetEnableReport `json:"skipped,omitempty"`
  1438. }
  1439. type BulkSetEnableReport struct {
  1440. Email string `json:"email"`
  1441. Reason string `json:"reason"`
  1442. }
  1443. func (s *ClientService) BulkSetEnable(inboundSvc *InboundService, emails []string, enable bool) (BulkSetEnableResult, bool, error) {
  1444. result := BulkSetEnableResult{}
  1445. cleanEmails := trimmedUniqueEmails(emails)
  1446. if len(cleanEmails) == 0 {
  1447. return result, false, nil
  1448. }
  1449. db := database.GetDB()
  1450. recordsByEmail, err := clientRecordsByEmail(db, cleanEmails)
  1451. if err != nil {
  1452. return result, false, err
  1453. }
  1454. skippedReasons := map[string]string{}
  1455. for _, email := range cleanEmails {
  1456. if _, ok := recordsByEmail[email]; !ok {
  1457. skippedReasons[email] = "client not found"
  1458. }
  1459. }
  1460. clientIds := make([]int, 0, len(recordsByEmail))
  1461. recordIdToEmail := make(map[int]string, len(recordsByEmail))
  1462. for _, r := range recordsByEmail {
  1463. clientIds = append(clientIds, r.Id)
  1464. recordIdToEmail[r.Id] = r.Email
  1465. }
  1466. emailsByInbound := map[int][]string{}
  1467. if len(clientIds) > 0 {
  1468. var mappings []model.ClientInbound
  1469. for _, batch := range chunkInts(clientIds, sqlInChunk) {
  1470. var rows []model.ClientInbound
  1471. if err := db.Where("client_id IN ?", batch).Find(&rows).Error; err != nil {
  1472. return result, false, err
  1473. }
  1474. mappings = append(mappings, rows...)
  1475. }
  1476. for _, m := range mappings {
  1477. email, ok := recordIdToEmail[m.ClientId]
  1478. if !ok {
  1479. continue
  1480. }
  1481. emailsByInbound[m.InboundId] = append(emailsByInbound[m.InboundId], email)
  1482. }
  1483. }
  1484. needRestart := false
  1485. enableIds := sortedInboundIds(emailsByInbound)
  1486. enableResults, enablePanics := fanoutInboundResults(enableIds, inboundFanoutConcurrency, func(i int) bulkSetEnableInboundResult {
  1487. return s.bulkSetEnableInboundClients(inboundSvc, enableIds[i], emailsByInbound[enableIds[i]], enable)
  1488. })
  1489. for i, ibRes := range enableResults {
  1490. if enablePanics[i] != nil {
  1491. needRestart = true
  1492. for _, email := range emailsByInbound[enableIds[i]] {
  1493. if _, already := skippedReasons[email]; !already {
  1494. skippedReasons[email] = enablePanics[i].Error()
  1495. }
  1496. }
  1497. continue
  1498. }
  1499. if ibRes.needRestart {
  1500. needRestart = true
  1501. }
  1502. for email, reason := range ibRes.perEmailSkipped {
  1503. if _, already := skippedReasons[email]; !already {
  1504. skippedReasons[email] = reason
  1505. }
  1506. }
  1507. }
  1508. successEmails := make([]string, 0, len(recordsByEmail))
  1509. for email := range recordsByEmail {
  1510. if _, skipped := skippedReasons[email]; skipped {
  1511. continue
  1512. }
  1513. successEmails = append(successEmails, email)
  1514. }
  1515. if len(successEmails) > 0 {
  1516. now := time.Now().UnixMilli()
  1517. if err := runSerializedTx(func(tx *gorm.DB) error {
  1518. for _, batch := range chunkStrings(successEmails, sqlInChunk) {
  1519. if e := tx.Model(xray.ClientTraffic{}).Where("email IN ?", batch).Update("enable", enable).Error; e != nil {
  1520. return e
  1521. }
  1522. if e := tx.Model(&model.ClientRecord{}).Where("email IN ?", batch).
  1523. Updates(map[string]any{"enable": enable, "updated_at": now}).Error; e != nil {
  1524. return e
  1525. }
  1526. }
  1527. return nil
  1528. }); err != nil {
  1529. return result, needRestart, err
  1530. }
  1531. }
  1532. result.Changed = len(successEmails)
  1533. for email, reason := range skippedReasons {
  1534. result.Skipped = append(result.Skipped, BulkSetEnableReport{Email: email, Reason: reason})
  1535. }
  1536. return result, needRestart, nil
  1537. }
  1538. type bulkSetEnableInboundResult struct {
  1539. perEmailSkipped map[string]string
  1540. needRestart bool
  1541. }
  1542. func (s *ClientService) bulkSetEnableInboundClients(inboundSvc *InboundService, inboundId int, emails []string, enable bool) bulkSetEnableInboundResult {
  1543. res := bulkSetEnableInboundResult{perEmailSkipped: map[string]string{}}
  1544. defer lockInbound(inboundId).Unlock()
  1545. oldInbound, err := inboundSvc.GetInbound(inboundId)
  1546. if err != nil {
  1547. for _, e := range emails {
  1548. res.perEmailSkipped[e] = err.Error()
  1549. }
  1550. return res
  1551. }
  1552. var settings map[string]any
  1553. if err := json.Unmarshal([]byte(oldInbound.Settings), &settings); err != nil {
  1554. for _, e := range emails {
  1555. res.perEmailSkipped[e] = err.Error()
  1556. }
  1557. return res
  1558. }
  1559. wanted := make(map[string]struct{}, len(emails))
  1560. for _, email := range emails {
  1561. wanted[email] = struct{}{}
  1562. }
  1563. cipher := ""
  1564. if oldInbound.Protocol == model.Shadowsocks {
  1565. cipher, _ = settings["method"].(string)
  1566. }
  1567. type changedClient struct {
  1568. email string
  1569. wasEnable bool
  1570. client model.Client
  1571. }
  1572. var changed []changedClient
  1573. found := map[string]bool{}
  1574. nowMs := time.Now().UnixMilli()
  1575. interfaceClients, _ := settings["clients"].([]any)
  1576. for i, c := range interfaceClients {
  1577. entry, ok := c.(map[string]any)
  1578. if !ok {
  1579. continue
  1580. }
  1581. email, _ := entry["email"].(string)
  1582. if _, want := wanted[email]; !want || email == "" {
  1583. continue
  1584. }
  1585. found[email] = true
  1586. prev, _ := entry["enable"].(bool)
  1587. if prev == enable {
  1588. continue
  1589. }
  1590. entry["enable"] = enable
  1591. entry["updated_at"] = nowMs
  1592. interfaceClients[i] = entry
  1593. // Build the pushed client from the inbound JSON (the per-inbound source of
  1594. // truth), so a remote UpdateUser carries every field and never zeroes
  1595. // subId/totalGB/expiry from drifting ClientRecord columns (#4628/#4792).
  1596. var parsed model.Client
  1597. if b, mErr := json.Marshal(entry); mErr == nil {
  1598. _ = json.Unmarshal(b, &parsed)
  1599. }
  1600. parsed.Email = email
  1601. parsed.Enable = enable
  1602. changed = append(changed, changedClient{email: email, wasEnable: prev, client: parsed})
  1603. }
  1604. for email := range wanted {
  1605. if !found[email] {
  1606. res.perEmailSkipped[email] = "Client Not Found In Inbound"
  1607. }
  1608. }
  1609. if len(changed) == 0 {
  1610. return res
  1611. }
  1612. settings["clients"] = interfaceClients
  1613. newSettings, err := json.MarshalIndent(settings, "", " ")
  1614. if err != nil {
  1615. for _, ch := range changed {
  1616. res.perEmailSkipped[ch.email] = err.Error()
  1617. }
  1618. return res
  1619. }
  1620. prevSettings := oldInbound.Settings
  1621. oldInbound.Settings = string(newSettings)
  1622. rt, push, _, perr := inboundSvc.nodePushPlan(oldInbound)
  1623. if perr != nil {
  1624. for _, ch := range changed {
  1625. res.perEmailSkipped[ch.email] = perr.Error()
  1626. }
  1627. return res
  1628. }
  1629. if oldInbound.NodeID != nil && push && len(changed) > nodeBulkPushThreshold {
  1630. push = false
  1631. }
  1632. txErr := runSerializedTx(func(tx *gorm.DB) error {
  1633. if e := tx.Save(oldInbound).Error; e != nil {
  1634. return e
  1635. }
  1636. finalClients, gcErr := inboundSvc.GetClients(oldInbound)
  1637. if gcErr != nil {
  1638. return gcErr
  1639. }
  1640. if err := s.SyncInbound(tx, inboundId, finalClients); err != nil {
  1641. return err
  1642. }
  1643. if oldInbound.NodeID != nil {
  1644. return (&NodeService{}).MarkNodeDirtyTx(tx, *oldInbound.NodeID)
  1645. }
  1646. return nil
  1647. })
  1648. if txErr != nil {
  1649. for _, ch := range changed {
  1650. res.perEmailSkipped[ch.email] = txErr.Error()
  1651. }
  1652. return res
  1653. }
  1654. if oldInbound.NodeID == nil {
  1655. if !push {
  1656. res.needRestart = true
  1657. } else {
  1658. for _, ch := range changed {
  1659. if enable {
  1660. err1 := rt.AddUser(context.Background(), oldInbound, map[string]any{
  1661. "email": ch.client.Email,
  1662. "id": ch.client.ID,
  1663. "security": ch.client.Security,
  1664. "flow": ch.client.Flow,
  1665. "auth": ch.client.Auth,
  1666. "password": ch.client.Password,
  1667. "cipher": cipher,
  1668. })
  1669. if err1 != nil {
  1670. logger.Debug("Error in adding client on", rt.Name(), ":", err1)
  1671. res.needRestart = true
  1672. }
  1673. } else if ch.wasEnable {
  1674. err1 := rt.RemoveUser(context.Background(), oldInbound, ch.email)
  1675. if err1 != nil && !strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", ch.email)) {
  1676. logger.Debug("Error in removing client on", rt.Name(), ":", err1)
  1677. res.needRestart = true
  1678. }
  1679. }
  1680. }
  1681. }
  1682. } else if push {
  1683. pushFailed := false
  1684. for _, ch := range changed {
  1685. updated := ch.client
  1686. updated.UpdatedAt = nowMs
  1687. ctx, cancel := nodePushContext()
  1688. err1 := rt.UpdateUser(ctx, oldInbound, ch.email, updated)
  1689. cancel()
  1690. if err1 != nil {
  1691. logger.Warning("Error in updating client on", rt.Name(), ":", err1)
  1692. pushFailed = true
  1693. // First failure ends the batch push; the reconcile converges the rest.
  1694. break
  1695. }
  1696. }
  1697. if !pushFailed {
  1698. advancePushedInbound(rt, prevSettings, oldInbound)
  1699. }
  1700. }
  1701. return res
  1702. }