client_crud.go 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174
  1. package service
  2. import (
  3. "encoding/base64"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "net/netip"
  8. "runtime/debug"
  9. "slices"
  10. "strings"
  11. "sync"
  12. "sync/atomic"
  13. "time"
  14. "unicode"
  15. "github.com/google/uuid"
  16. "github.com/mhsanaei/3x-ui/v3/internal/database"
  17. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  18. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  19. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  20. "github.com/mhsanaei/3x-ui/v3/internal/util/random"
  21. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  22. "gorm.io/gorm"
  23. )
  24. func hasForbiddenClientChar(s string) bool {
  25. for _, r := range s {
  26. if r == '/' || r == '\\' || r < 0x20 || r == 0x7f || unicode.IsSpace(r) {
  27. return true
  28. }
  29. }
  30. return false
  31. }
  32. func validateClientEmail(email string) error {
  33. if hasForbiddenClientChar(email) {
  34. return common.NewError("client email contains an invalid character:", email)
  35. }
  36. return nil
  37. }
  38. func validateClientSubID(subID string) error {
  39. if hasForbiddenClientChar(subID) {
  40. return common.NewError("client subId contains an invalid character:", subID)
  41. }
  42. return nil
  43. }
  44. // Rejected rather than coerced: an unknown cycle would leave the operator with
  45. // a field that reads as configured while no job ever selects the client.
  46. func validateClientTrafficReset(period string, day int) error {
  47. switch period {
  48. case "", "never", "hourly", "daily", "weekly", "monthly":
  49. default:
  50. return common.NewError("client trafficReset must be never, hourly, daily, weekly or monthly, got:", period)
  51. }
  52. if day < 0 || day > 31 {
  53. return common.NewError("client trafficResetDay must be between 0 and 31, got:", day)
  54. }
  55. return nil
  56. }
  57. // Rejected rather than clamped: nextCalendarRenewal would silently move an
  58. // out-of-range day, and a negative one drops out of the renewal query entirely.
  59. func validateClientResetDay(day int) error {
  60. if day < 0 || day > 31 {
  61. return common.NewError("client resetDay must be between 0 and 31, got:", day)
  62. }
  63. return nil
  64. }
  65. // Rejected rather than coerced: a negative cap reads as "unlimited" to a caller
  66. // but selects nothing, so the client would silently stop renewing.
  67. func validateClientResetMax(resetMax int) error {
  68. if resetMax < 0 {
  69. return common.NewError("client resetMax must not be negative, got:", resetMax)
  70. }
  71. return nil
  72. }
  73. // normalizeClientTrafficReset stores what the inbound path would store, so the
  74. // day never reaches the DB as a 0 that three layers downstream each clamp to 1.
  75. func normalizeClientTrafficReset(c *model.Client) {
  76. if c.TrafficReset == "" {
  77. c.TrafficReset = "never"
  78. }
  79. c.TrafficResetDay = normalizeTrafficResetDay(c.TrafficResetDay)
  80. }
  81. // ClientResetCycle is the slice of a client the reset job needs: enough to know
  82. // whether it is due, and whether its disable is the quota's doing or the operator's.
  83. type ClientResetCycle struct {
  84. Email string
  85. TrafficResetDay int
  86. Enable bool
  87. Total int64
  88. Used int64
  89. }
  90. // Depleted reports a client the quota switched off. A reset restores that one;
  91. // a client disabled below its quota was switched off by hand and stays off.
  92. func (c ClientResetCycle) Depleted() bool {
  93. return c.Total > 0 && c.Used >= c.Total
  94. }
  95. // GetClientsByTrafficReset returns the clients whose own reset cycle matches the
  96. // period, independent of the cycle configured on the inbounds they belong to.
  97. func (s *ClientService) GetClientsByTrafficReset(period string) ([]ClientResetCycle, error) {
  98. var cycles []ClientResetCycle
  99. err := database.GetDB().Table("clients c").
  100. Select("c.email, c.traffic_reset_day, c.enable, COALESCE(ct.total, 0) AS total, COALESCE(ct.up, 0) + COALESCE(ct.down, 0) AS used").
  101. Joins("LEFT JOIN client_traffics ct ON ct.email = c.email").
  102. Where("c.traffic_reset = ?", period).
  103. Scan(&cycles).Error
  104. if err != nil {
  105. return nil, err
  106. }
  107. return cycles, nil
  108. }
  109. // Create applies the client to every requested inbound: one failing inbound no
  110. // longer aborts the others, so the error can name several and needRestart holds.
  111. func (s *ClientService) Create(inboundSvc *InboundService, payload *ClientCreatePayload) (bool, error) {
  112. if payload == nil {
  113. return false, common.NewError("empty payload")
  114. }
  115. client := payload.Client
  116. if strings.TrimSpace(client.Email) == "" {
  117. return false, common.NewError("client email is required")
  118. }
  119. if err := validateClientEmail(client.Email); err != nil {
  120. return false, err
  121. }
  122. if err := validateClientSubID(client.SubID); err != nil {
  123. return false, err
  124. }
  125. if err := validateClientResetDay(client.ResetDay); err != nil {
  126. return false, err
  127. }
  128. if err := validateClientResetMax(client.ResetMax); err != nil {
  129. return false, err
  130. }
  131. if err := validateClientTrafficReset(client.TrafficReset, client.TrafficResetDay); err != nil {
  132. return false, err
  133. }
  134. normalizeClientTrafficReset(&client)
  135. if len(payload.InboundIds) == 0 {
  136. return false, common.NewError("at least one inbound is required")
  137. }
  138. if client.SubID == "" {
  139. client.SubID = uuid.NewString()
  140. }
  141. if !client.Enable {
  142. client.Enable = true
  143. }
  144. now := time.Now().UnixMilli()
  145. if client.CreatedAt == 0 {
  146. client.CreatedAt = now
  147. }
  148. client.UpdatedAt = now
  149. existing := &model.ClientRecord{}
  150. err := database.GetDB().Where("email = ?", client.Email).First(existing).Error
  151. if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
  152. return false, err
  153. }
  154. emailTaken := !errors.Is(err, gorm.ErrRecordNotFound)
  155. if emailTaken {
  156. if existing.SubID == "" || existing.SubID != client.SubID {
  157. return false, common.NewError("email already in use:", client.Email)
  158. }
  159. // Reuse stored credentials when re-adding an existing identity, or
  160. // fillProtocolDefaults mints a fresh UUID that desyncs other inbounds.
  161. if client.ID == "" {
  162. client.ID = existing.UUID
  163. }
  164. if client.Password == "" {
  165. client.Password = existing.Password
  166. }
  167. if client.Auth == "" {
  168. client.Auth = existing.Auth
  169. }
  170. if client.Secret == "" {
  171. client.Secret = existing.Secret
  172. }
  173. }
  174. if client.SubID != "" {
  175. var subTaken int64
  176. if err := database.GetDB().Model(&model.ClientRecord{}).
  177. Where("sub_id = ? AND email <> ?", client.SubID, client.Email).
  178. Count(&subTaken).Error; err != nil {
  179. return false, err
  180. }
  181. if subTaken > 0 {
  182. return false, common.NewError("subId already in use:", client.SubID)
  183. }
  184. }
  185. // Prepared before any inbound is written: fillProtocolDefaults mints the
  186. // shared credentials on the first inbound and every later one reuses them.
  187. adds := make([]*model.Inbound, 0, len(payload.InboundIds))
  188. for _, ibId := range payload.InboundIds {
  189. inbound, getErr := inboundSvc.GetInbound(ibId)
  190. if getErr != nil {
  191. return false, fmt.Errorf("inbound %d: %w", ibId, getErr)
  192. }
  193. if err := s.fillProtocolDefaults(&client, inbound); err != nil {
  194. return false, fmt.Errorf("inbound %d: %w", ibId, err)
  195. }
  196. clientForInbound := client
  197. if ips, ok := client.AllowedIPsByInbound[ibId]; ok {
  198. clientForInbound.AllowedIPs = ips
  199. } else if !addressesFitAmneziaWGInbound(clientForInbound.AllowedIPs, inbound) {
  200. // The shared AllowedIPs value (e.g. from a single-field legacy
  201. // caller) came from a different subnet than this inbound's own --
  202. // clear it so defaultAmneziaWGClients allocates a fresh, correct
  203. // address for THIS inbound instead of persisting an unroutable
  204. // peer. Same reasoning as addressesFitAmneziaWGInbound's own doc
  205. // comment on the Attach path.
  206. clientForInbound.AllowedIPs = nil
  207. }
  208. settingsPayload, mErr := json.Marshal(map[string][]model.Client{"clients": {clientWithInboundFlow(clientForInbound, inbound)}})
  209. if mErr != nil {
  210. return false, fmt.Errorf("inbound %d: %w", ibId, mErr)
  211. }
  212. adds = append(adds, &model.Inbound{Id: ibId, Settings: string(settingsPayload)})
  213. }
  214. needRestart, fanoutErr := s.fanoutInboundClientAdds(inboundSvc, adds)
  215. if fanoutErr != nil {
  216. // Never on a failed create: this retrims the devices of an email that
  217. // already existed, and a create the panel reported as failed must not.
  218. return needRestart, fanoutErr
  219. }
  220. // A re-created email is a live identity again: a delete tombstone left
  221. // standing makes the next node merge prune the new client's inbound links.
  222. withdrawClientTombstones(client.Email)
  223. return needRestart, s.setClientLimitHwidByEmail(nil, client.Email, payload.LimitHwid)
  224. }
  225. // inboundFanoutConcurrency caps how many inbounds one client op applies at
  226. // once, so a client spanning many of them can't start an unbounded RPC burst.
  227. const inboundFanoutConcurrency = 4
  228. // inboundApply is one inbound's share of a client op, ready to run.
  229. type inboundApply struct {
  230. id int
  231. run func() (bool, error)
  232. }
  233. // fanoutInboundApplies runs the applies with the node pushes overlapping, so a
  234. // client spanning several nodes no longer costs one RPC round-trip per node.
  235. func fanoutInboundApplies(applies []inboundApply) (bool, error) {
  236. var needRestart atomic.Bool
  237. errs := make([]error, len(applies))
  238. sem := make(chan struct{}, inboundFanoutConcurrency)
  239. var wg sync.WaitGroup
  240. for i := range applies {
  241. wg.Add(1)
  242. sem <- struct{}{}
  243. go func() {
  244. defer wg.Done()
  245. defer func() { <-sem }()
  246. // Off the request goroutine gin's Recovery no longer covers this,
  247. // so an unrecovered panic here would take the whole panel down.
  248. defer func() {
  249. if r := recover(); r != nil {
  250. // The apply may already have committed, so ask for the
  251. // restart the lost return value can no longer report.
  252. needRestart.Store(true)
  253. errs[i] = fmt.Errorf("inbound %d: panic: %v", applies[i].id, r)
  254. logger.Errorf("panic applying client change to inbound %d: %v\n%s", applies[i].id, r, debug.Stack())
  255. }
  256. }()
  257. nr, err := applies[i].run()
  258. if nr {
  259. needRestart.Store(true)
  260. }
  261. if err != nil {
  262. errs[i] = fmt.Errorf("inbound %d: %w", applies[i].id, err)
  263. }
  264. }()
  265. }
  266. wg.Wait()
  267. return needRestart.Load(), errors.Join(errs...)
  268. }
  269. // fanoutInboundClientAdds applies one payload per inbound.
  270. func (s *ClientService) fanoutInboundClientAdds(inboundSvc *InboundService, adds []*model.Inbound) (bool, error) {
  271. applies := make([]inboundApply, 0, len(adds))
  272. for _, add := range adds {
  273. applies = append(applies, inboundApply{id: add.Id, run: func() (bool, error) {
  274. return s.AddInboundClient(inboundSvc, add)
  275. }})
  276. }
  277. return fanoutInboundApplies(applies)
  278. }
  279. // fanoutInboundResults runs one job per inbound with the node pushes
  280. // overlapping, so a bulk op costs one RPC round-trip instead of one per node.
  281. // limit is the caller's own cap: an op that allocates tunnel addresses passes 1,
  282. // because allocation reads a cross-inbound used-set before it writes.
  283. func fanoutInboundResults[T any](inboundIds []int, limit int, run func(i int) T) ([]T, []error) {
  284. if limit < 1 {
  285. limit = 1
  286. }
  287. out := make([]T, len(inboundIds))
  288. errs := make([]error, len(inboundIds))
  289. sem := make(chan struct{}, limit)
  290. var wg sync.WaitGroup
  291. for i := range inboundIds {
  292. wg.Add(1)
  293. sem <- struct{}{}
  294. go func() {
  295. defer wg.Done()
  296. defer func() { <-sem }()
  297. // Off the request goroutine gin's Recovery no longer covers this,
  298. // so an unrecovered panic here would take the whole panel down.
  299. defer func() {
  300. if r := recover(); r != nil {
  301. errs[i] = fmt.Errorf("inbound %d: panic: %v", inboundIds[i], r)
  302. logger.Errorf("panic applying bulk client change to inbound %d: %v\n%s", inboundIds[i], r, debug.Stack())
  303. }
  304. }()
  305. out[i] = run(i)
  306. }()
  307. }
  308. wg.Wait()
  309. return out, errs
  310. }
  311. // addFanoutLimit serializes an add that touches a tunnel inbound. WireGuard and
  312. // AmneziaWG pick a free peer address by reading every inbound's used-set first,
  313. // so two overlapping allocations hand out the same one and the second is refused.
  314. func addFanoutLimit(anyTunnel bool) int {
  315. if anyTunnel {
  316. return 1
  317. }
  318. return inboundFanoutConcurrency
  319. }
  320. // sortedInboundIds gives the fanout a stable order, so which inbound wins a
  321. // per-email report no longer depends on Go's map iteration order.
  322. func sortedInboundIds[V any](byInbound map[int]V) []int {
  323. ids := make([]int, 0, len(byInbound))
  324. for id := range byInbound {
  325. ids = append(ids, id)
  326. }
  327. slices.Sort(ids)
  328. return ids
  329. }
  330. // markInboundNodesDirty makes a half-applied client edit unobservable to a node
  331. // snapshot merge, which skips a node whose config is already flagged dirty.
  332. func markInboundNodesDirty(inboundIds []int) error {
  333. if len(inboundIds) == 0 {
  334. return nil
  335. }
  336. var nodeIDs []int
  337. for _, batch := range chunkInts(inboundIds, sqlInChunk) {
  338. var ids []int
  339. if err := database.GetDB().Model(&model.Inbound{}).
  340. Where("id IN ? AND node_id IS NOT NULL", batch).
  341. Distinct().Pluck("node_id", &ids).Error; err != nil {
  342. return err
  343. }
  344. nodeIDs = append(nodeIDs, ids...)
  345. }
  346. if len(nodeIDs) == 0 {
  347. return nil
  348. }
  349. return runSerializedTx(func(tx *gorm.DB) error {
  350. svc := &NodeService{}
  351. for _, id := range nodeIDs {
  352. if err := svc.MarkNodeDirtyTx(tx, id); err != nil {
  353. return err
  354. }
  355. }
  356. return nil
  357. })
  358. }
  359. func (s *ClientService) fillProtocolDefaults(c *model.Client, ib *model.Inbound) error {
  360. switch ib.Protocol {
  361. case model.VMESS, model.VLESS:
  362. if c.ID == "" {
  363. c.ID = uuid.NewString()
  364. }
  365. case model.Trojan:
  366. if c.Password == "" {
  367. c.Password = strings.ReplaceAll(uuid.NewString(), "-", "")
  368. }
  369. case model.Shadowsocks:
  370. method := shadowsocksMethodFromSettings(ib.Settings)
  371. if c.Password == "" || !validShadowsocksClientKey(method, c.Password) {
  372. c.Password = randomShadowsocksClientKey(method)
  373. }
  374. case model.Hysteria:
  375. if c.Auth == "" {
  376. c.Auth = strings.ReplaceAll(uuid.NewString(), "-", "")
  377. }
  378. case model.MTProto:
  379. if c.Secret == "" {
  380. c.Secret = model.GenerateFakeTLSSecret(mtprotoDomainFromSettings(ib.Settings))
  381. }
  382. }
  383. return nil
  384. }
  385. // defaultMtprotoDomain is the FakeTLS fronting domain used when an mtproto
  386. // inbound carries no fakeTlsDomain of its own; it mirrors the frontend default.
  387. const defaultMtprotoDomain = "www.cloudflare.com"
  388. // mtprotoDomainFromSettings returns the inbound-level FakeTLS domain, falling
  389. // back to the default when unset, so a generated client secret always fronts a
  390. // real hostname.
  391. func mtprotoDomainFromSettings(settings string) string {
  392. domain := ""
  393. if settings != "" {
  394. var m map[string]any
  395. if err := json.Unmarshal([]byte(settings), &m); err == nil {
  396. domain, _ = m["fakeTlsDomain"].(string)
  397. }
  398. }
  399. domain = strings.TrimSpace(domain)
  400. if domain == "" {
  401. return defaultMtprotoDomain
  402. }
  403. return domain
  404. }
  405. func clientWithInboundFlow(c model.Client, ib *model.Inbound) model.Client {
  406. if ib.DisableFlow || !inboundCanEnableTlsFlow(string(ib.Protocol), ib.StreamSettings, ib.Settings) {
  407. c.Flow = ""
  408. }
  409. return c
  410. }
  411. func shadowsocksMethodFromSettings(settings string) string {
  412. if settings == "" {
  413. return ""
  414. }
  415. var m map[string]any
  416. if err := json.Unmarshal([]byte(settings), &m); err != nil {
  417. return ""
  418. }
  419. method, _ := m["method"].(string)
  420. return method
  421. }
  422. func randomShadowsocksClientKey(method string) string {
  423. if n := shadowsocksKeyBytes(method); n > 0 {
  424. return random.Base64Bytes(n)
  425. }
  426. return strings.ReplaceAll(uuid.NewString(), "-", "")
  427. }
  428. func validShadowsocksClientKey(method, key string) bool {
  429. n := shadowsocksKeyBytes(method)
  430. if n == 0 {
  431. return key != ""
  432. }
  433. decoded, err := base64.StdEncoding.DecodeString(key)
  434. if err != nil {
  435. return false
  436. }
  437. return len(decoded) == n
  438. }
  439. func shadowsocksKeyBytes(method string) int {
  440. switch method {
  441. case "2022-blake3-aes-128-gcm":
  442. return 16
  443. case "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305":
  444. return 32
  445. }
  446. return 0
  447. }
  448. // normalizeShadowsocksClientKeys rewrites any Shadowsocks-2022 client password
  449. // whose decoded length no longer matches settings.method, which happens after the
  450. // inbound method is switched between ciphers of different key sizes (e.g.
  451. // aes-256↔aes-128). A wrong-length uPSK makes xray reject the user, so the link
  452. // fails to connect; regenerating restores a valid key (clients must re-fetch).
  453. // Non-Shadowsocks / legacy-SS settings pass through unchanged.
  454. func normalizeShadowsocksClientKeys(settings string) (string, bool) {
  455. method := shadowsocksMethodFromSettings(settings)
  456. if shadowsocksKeyBytes(method) == 0 {
  457. return settings, false
  458. }
  459. var m map[string]any
  460. if err := json.Unmarshal([]byte(settings), &m); err != nil {
  461. return settings, false
  462. }
  463. clients, ok := m["clients"].([]any)
  464. if !ok {
  465. return settings, false
  466. }
  467. changed := false
  468. for i := range clients {
  469. c, ok := clients[i].(map[string]any)
  470. if !ok {
  471. continue
  472. }
  473. if pw, _ := c["password"].(string); validShadowsocksClientKey(method, pw) {
  474. continue
  475. }
  476. c["password"] = randomShadowsocksClientKey(method)
  477. clients[i] = c
  478. changed = true
  479. }
  480. if !changed {
  481. return settings, false
  482. }
  483. m["clients"] = clients
  484. bs, err := json.MarshalIndent(m, "", " ")
  485. if err != nil {
  486. return settings, false
  487. }
  488. return string(bs), true
  489. }
  490. func applyShadowsocksClientMethod(clients []any, settings map[string]any) {
  491. method, _ := settings["method"].(string)
  492. is2022 := strings.HasPrefix(method, "2022-blake3-")
  493. for i := range clients {
  494. cm, ok := clients[i].(map[string]any)
  495. if !ok {
  496. continue
  497. }
  498. if is2022 {
  499. if _, hasKey := cm["method"]; hasKey {
  500. delete(cm, "method")
  501. clients[i] = cm
  502. }
  503. continue
  504. }
  505. if method == "" {
  506. continue
  507. }
  508. if existing, _ := cm["method"].(string); existing != "" {
  509. continue
  510. }
  511. cm["method"] = method
  512. clients[i] = cm
  513. }
  514. }
  515. func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model.Client, limitHwid int, inboundFilter ...int) (bool, error) {
  516. existing, err := s.GetByID(id)
  517. if err != nil {
  518. return false, err
  519. }
  520. inboundIds, err := s.GetInboundIdsForRecord(id)
  521. if err != nil {
  522. return false, err
  523. }
  524. // The rename rewrites the one shared client record, so every node holding
  525. // this client goes stale — not just the ones an inboundIds filter applies.
  526. attachedIds := append([]int(nil), inboundIds...)
  527. if len(inboundFilter) > 0 {
  528. allow := make(map[int]struct{}, len(inboundFilter))
  529. for _, fid := range inboundFilter {
  530. allow[fid] = struct{}{}
  531. }
  532. filtered := inboundIds[:0:0]
  533. for _, ibId := range inboundIds {
  534. if _, ok := allow[ibId]; ok {
  535. filtered = append(filtered, ibId)
  536. }
  537. }
  538. inboundIds = filtered
  539. }
  540. if strings.TrimSpace(updated.Email) == "" {
  541. return false, common.NewError("client email is required")
  542. }
  543. if err := validateClientEmail(updated.Email); err != nil {
  544. return false, err
  545. }
  546. if err := validateClientSubID(updated.SubID); err != nil {
  547. return false, err
  548. }
  549. if err := validateClientResetDay(updated.ResetDay); err != nil {
  550. return false, err
  551. }
  552. if err := validateClientResetMax(updated.ResetMax); err != nil {
  553. return false, err
  554. }
  555. if err := validateClientTrafficReset(updated.TrafficReset, updated.TrafficResetDay); err != nil {
  556. return false, err
  557. }
  558. normalizeClientTrafficReset(&updated)
  559. if updated.SubID == "" {
  560. updated.SubID = existing.SubID
  561. }
  562. if updated.SubID == "" {
  563. updated.SubID = uuid.NewString()
  564. }
  565. updated.UpdatedAt = time.Now().UnixMilli()
  566. if updated.CreatedAt == 0 {
  567. updated.CreatedAt = existing.CreatedAt
  568. }
  569. // Preserve existing credentials when the caller omits them, so a partial
  570. // update (e.g. only changing traffic/expiry) doesn't silently rotate the
  571. // client's UUID/password/auth via fillProtocolDefaults. Supplying a new
  572. // value still rotates it intentionally.
  573. if updated.ID == "" {
  574. updated.ID = existing.UUID
  575. }
  576. if updated.Password == "" {
  577. updated.Password = existing.Password
  578. }
  579. if updated.Auth == "" {
  580. updated.Auth = existing.Auth
  581. }
  582. if updated.Secret == "" {
  583. updated.Secret = existing.Secret
  584. }
  585. if updated.Email != existing.Email {
  586. var collisionCount int64
  587. if err := database.GetDB().Model(&model.ClientRecord{}).
  588. Where("email = ? AND id <> ?", updated.Email, id).
  589. Count(&collisionCount).Error; err != nil {
  590. return false, err
  591. }
  592. if collisionCount > 0 {
  593. return false, common.NewError("Duplicate email:", updated.Email)
  594. }
  595. }
  596. if updated.SubID != existing.SubID {
  597. var subCollision int64
  598. if err := database.GetDB().Model(&model.ClientRecord{}).
  599. Where("sub_id = ? AND id <> ?", updated.SubID, id).
  600. Count(&subCollision).Error; err != nil {
  601. return false, err
  602. }
  603. if subCollision > 0 {
  604. return false, common.NewError("Duplicate subId:", updated.SubID)
  605. }
  606. }
  607. tunnelCount, tcErr := tunnelInboundCount(inboundIds)
  608. if tcErr != nil {
  609. return false, tcErr
  610. }
  611. // Built before any inbound is written, as in Create: fillProtocolDefaults
  612. // mints the shared credentials on the first inbound, later ones reuse them.
  613. applies := make([]inboundApply, 0, len(inboundIds))
  614. for _, ibId := range inboundIds {
  615. inbound, getErr := inboundSvc.GetInbound(ibId)
  616. if getErr != nil {
  617. if errors.Is(getErr, gorm.ErrRecordNotFound) {
  618. if err := database.GetDB().
  619. Where("client_id = ? AND inbound_id = ?", id, ibId).
  620. Delete(&model.ClientInbound{}).Error; err != nil {
  621. return false, err
  622. }
  623. continue
  624. }
  625. return false, getErr
  626. }
  627. if existing.Email == "" {
  628. continue
  629. }
  630. if err := s.fillProtocolDefaults(&updated, inbound); err != nil {
  631. return false, err
  632. }
  633. clientForInbound := updated
  634. if ips, ok := updated.AllowedIPsByInbound[ibId]; ok {
  635. clientForInbound.AllowedIPs = ips
  636. } else if tunnelCount > 1 && (inbound.Protocol == model.WireGuard || inbound.Protocol == model.AmneziaWG) {
  637. // One shared peer field set cannot describe several peers: broadcast
  638. // it and they all end up with the same keys and tunnel address.
  639. clientForInbound.AllowedIPs = nil
  640. clientForInbound.PrivateKey = ""
  641. clientForInbound.PublicKey = ""
  642. clientForInbound.PreSharedKey = ""
  643. } else if !addressesFitAmneziaWGInbound(clientForInbound.AllowedIPs, inbound) {
  644. // A single shared AllowedIPs field (the common case for a caller
  645. // that never sends AllowedIPsByInbound) must never overwrite an
  646. // inbound it doesn't belong to -- e.g. a client attached to both
  647. // wg and awg saving its wg-labeled address would otherwise get
  648. // that same address silently written into the awg peer config
  649. // too. Clearing it here makes UpdateInboundClient's own
  650. // empty-AllowedIPs carry-forward (see its WireGuard/AmneziaWG
  651. // branch) preserve THIS inbound's existing, correct value
  652. // instead.
  653. clientForInbound.AllowedIPs = nil
  654. }
  655. settingsPayload, mErr := json.Marshal(map[string][]model.Client{"clients": {clientWithInboundFlow(clientForInbound, inbound)}})
  656. if mErr != nil {
  657. return false, mErr
  658. }
  659. data := &model.Inbound{Id: ibId, Settings: string(settingsPayload)}
  660. applies = append(applies, inboundApply{id: ibId, run: func() (bool, error) {
  661. return s.UpdateInboundClient(inboundSvc, data, existing.Email)
  662. }})
  663. }
  664. // Each apply marks only its OWN node dirty, so between the first and last
  665. // one a merge could resurrect the pre-edit email as a second client.
  666. if err := markInboundNodesDirty(attachedIds); err != nil {
  667. return false, err
  668. }
  669. needRestart, applyErr := fanoutInboundApplies(applies)
  670. if applyErr != nil {
  671. return needRestart, applyErr
  672. }
  673. // UpdateInboundClient renames the record atomically with each inbound's
  674. // settings JSON; this direct write only covers records with no inbound left.
  675. if updated.Email != existing.Email {
  676. if err := database.GetDB().Model(&model.ClientRecord{}).
  677. Where("id = ? AND email = ?", id, existing.Email).
  678. Update("email", updated.Email).Error; err != nil {
  679. return needRestart, err
  680. }
  681. }
  682. if len(inboundIds) == 0 {
  683. merged := *existing
  684. applyClientRecordMerge(&merged, updated.ToRecord())
  685. if err := database.GetDB().Model(&model.ClientRecord{}).
  686. Where("id = ?", id).
  687. Updates(map[string]any{
  688. "sub_id": merged.SubID,
  689. "uuid": merged.UUID,
  690. "password": merged.Password,
  691. "auth": merged.Auth,
  692. "secret": merged.Secret,
  693. "flow": merged.Flow,
  694. "security": merged.Security,
  695. "wg_private_key": merged.PrivateKey,
  696. "wg_public_key": merged.PublicKey,
  697. "wg_allowed_ips": merged.AllowedIPs,
  698. "wg_pre_shared_key": merged.PreSharedKey,
  699. "wg_keep_alive": merged.KeepAlive,
  700. "limit_ip": merged.LimitIP,
  701. "total_gb": merged.TotalGB,
  702. "expiry_time": merged.ExpiryTime,
  703. "tg_id": merged.TgID,
  704. "comment": merged.Comment,
  705. "reset": merged.Reset,
  706. "reset_day": merged.ResetDay,
  707. "reset_max": merged.ResetMax,
  708. "traffic_reset": merged.TrafficReset,
  709. "traffic_reset_day": merged.TrafficResetDay,
  710. }).Error; err != nil {
  711. return needRestart, err
  712. }
  713. }
  714. reverseStr := ""
  715. if updated.Reverse != nil && strings.TrimSpace(updated.Reverse.Tag) != "" {
  716. if b, mErr := json.Marshal(updated.Reverse); mErr == nil {
  717. reverseStr = string(b)
  718. }
  719. }
  720. if err := database.GetDB().Model(&model.ClientRecord{}).
  721. Where("id = ?", id).
  722. Update("reverse", reverseStr).Error; err != nil {
  723. return needRestart, err
  724. }
  725. // Persist the group explicitly. SyncInbound deliberately preserves the
  726. // stored group when the inbound settings carry none — so a node snapshot or a
  727. // group-less settings rebuild can't wipe it (see SyncInbound + its tests).
  728. // That guard also meant clearing the group in the client editor never took
  729. // effect. The editor always round-trips the field, so apply it here,
  730. // including the empty string that removes the client from its group.
  731. if err := database.GetDB().Model(&model.ClientRecord{}).
  732. Where("id = ?", id).
  733. UpdateColumn("group_name", updated.Group).Error; err != nil {
  734. return needRestart, err
  735. }
  736. // Same shape as the group write above: SyncInbound keeps a stored ad-tag
  737. // when the incoming settings carry none, so clearing the override must be
  738. // applied here, where the editor always round-trips the field.
  739. if err := database.GetDB().Model(&model.ClientRecord{}).
  740. Where("id = ?", id).
  741. UpdateColumn("ad_tag", updated.AdTag).Error; err != nil {
  742. return needRestart, err
  743. }
  744. if err := database.GetDB().Model(&model.ClientRecord{}).
  745. Where("id = ?", id).
  746. UpdateColumn("enable", updated.Enable).Error; err != nil {
  747. return needRestart, err
  748. }
  749. if err := s.setClientLimitHwidByEmail(nil, updated.Email, limitHwid); err != nil {
  750. return needRestart, err
  751. }
  752. if err := database.GetDB().Model(&model.ClientRecord{}).
  753. Where("id = ?", id).
  754. UpdateColumn("updated_at", time.Now().UnixMilli()).Error; err != nil {
  755. return needRestart, err
  756. }
  757. return needRestart, nil
  758. }
  759. func (s *ClientService) Delete(inboundSvc *InboundService, id int, keepTraffic bool) (bool, error) {
  760. existing, err := s.GetByID(id)
  761. if err != nil {
  762. return false, err
  763. }
  764. tombstoneClientEmail(existing.Email)
  765. inboundIds, err := s.GetInboundIdsForRecord(id)
  766. if err != nil {
  767. withdrawClientTombstones(existing.Email)
  768. return false, err
  769. }
  770. applies := make([]inboundApply, 0, len(inboundIds))
  771. var delErrs []error
  772. for _, ibId := range inboundIds {
  773. if _, getErr := inboundSvc.GetInbound(ibId); getErr != nil {
  774. if errors.Is(getErr, gorm.ErrRecordNotFound) {
  775. continue
  776. }
  777. delErrs = append(delErrs, fmt.Errorf("inbound %d: %w", ibId, getErr))
  778. continue
  779. }
  780. // Always delete by email — the client's stable identity. This removes
  781. // every matching entry from the inbound's settings even when the stored
  782. // credential (UUID/password/auth) drifted from the inbound JSON, or a
  783. // duplicate entry with the same email exists.
  784. if existing.Email == "" {
  785. continue
  786. }
  787. applies = append(applies, inboundApply{id: ibId, run: func() (bool, error) {
  788. nr, delErr := s.DelInboundClientByEmail(inboundSvc, ibId, existing.Email, keepTraffic, true)
  789. // The client is already absent from this inbound (data drift or a
  790. // retried delete). Skip it — deletion stays idempotent.
  791. if errors.Is(delErr, ErrClientNotInInbound) {
  792. return nr, nil
  793. }
  794. return nr, delErr
  795. }})
  796. }
  797. needRestart, applyErr := fanoutInboundApplies(applies)
  798. if applyErr != nil {
  799. delErrs = append(delErrs, applyErr)
  800. }
  801. // A failed inbound still holds the client in its settings JSON: keep the
  802. // record so the next delete retries exactly the leftovers, and report it.
  803. // The tombstone lifts with it, or the next node merge finishes the deletion.
  804. if len(delErrs) > 0 {
  805. withdrawClientTombstones(existing.Email)
  806. return needRestart, errors.Join(delErrs...)
  807. }
  808. db := database.GetDB()
  809. if err := db.Transaction(func(tx *gorm.DB) error {
  810. if existing.Email != "" {
  811. if err := adjustGroupBaselinesForRemovedTraffic(tx, []string{existing.Email}); err != nil {
  812. return err
  813. }
  814. }
  815. if err := tx.Where("client_id = ?", id).Delete(&model.ClientInbound{}).Error; err != nil {
  816. return err
  817. }
  818. if err := tx.Where("client_id = ?", id).Delete(&model.ClientExternalLink{}).Error; err != nil {
  819. return err
  820. }
  821. if err := clearClientHwidsBySubIDTx(tx, existing.SubID); err != nil {
  822. return err
  823. }
  824. if !keepTraffic && existing.Email != "" {
  825. if err := tx.Where("email = ?", existing.Email).Delete(&xray.ClientTraffic{}).Error; err != nil {
  826. return err
  827. }
  828. if err := clearGlobalTraffic(tx, existing.Email); err != nil {
  829. return err
  830. }
  831. if err := tx.Where("client_email = ?", existing.Email).Delete(&model.InboundClientIps{}).Error; err != nil {
  832. return err
  833. }
  834. if err := tx.Where("email = ?", existing.Email).Delete(&model.NodeClientTraffic{}).Error; err != nil {
  835. return err
  836. }
  837. }
  838. return tx.Delete(&model.ClientRecord{}, id).Error
  839. }); err != nil {
  840. withdrawClientTombstones(existing.Email)
  841. return needRestart, err
  842. }
  843. return needRestart, nil
  844. }
  845. // hasTunnelAttachment reports whether any of inboundIds is a currently
  846. // existing WireGuard or AmneziaWG inbound. Inbounds that fail to load are
  847. // skipped rather than treated as an error -- Attach's own loop already
  848. // surfaces a real error for any inbound it can't load when it gets there.
  849. func (s *ClientService) hasTunnelAttachment(inboundSvc *InboundService, inboundIds []int) bool {
  850. for _, ibId := range inboundIds {
  851. inbound, err := inboundSvc.GetInbound(ibId)
  852. if err != nil {
  853. continue
  854. }
  855. if inbound.Protocol == model.WireGuard || inbound.Protocol == model.AmneziaWG {
  856. return true
  857. }
  858. }
  859. return false
  860. }
  861. // tunnelInboundCount reports how many of inboundIds are WireGuard/AmneziaWG,
  862. // i.e. how many independent peers one shared field set would be written to.
  863. func tunnelInboundCount(inboundIds []int) (int64, error) {
  864. if len(inboundIds) == 0 {
  865. return 0, nil
  866. }
  867. var n int64
  868. err := database.GetDB().Model(&model.Inbound{}).
  869. Where("id IN ? AND protocol IN ?", inboundIds, []model.Protocol{model.WireGuard, model.AmneziaWG}).
  870. Count(&n).Error
  871. return n, err
  872. }
  873. // addressesFitAmneziaWGInbound reports whether every entry in addrs falls
  874. // inside ib's own configured subnet(s). AmneziaWG only: its kernel interface
  875. // Address is exactly that subnet, so an address inherited from elsewhere (an
  876. // identity attached to a WireGuard inbound first, say) produces a peer that
  877. // can never connect -- Attach allocates fresh instead.
  878. func addressesFitAmneziaWGInbound(addrs []string, ib *model.Inbound) bool {
  879. if ib.Protocol != model.AmneziaWG || len(addrs) == 0 {
  880. return true
  881. }
  882. v4Base, v6Base, err := defaultAmneziaWGSubnetBases(ib.Settings)
  883. if err != nil {
  884. return false
  885. }
  886. bases := make([]netip.Prefix, 0, 2)
  887. for _, base := range []string{v4Base, v6Base} {
  888. if base == "" {
  889. continue
  890. }
  891. prefix, pErr := netip.ParsePrefix(base)
  892. if pErr != nil {
  893. return false
  894. }
  895. bases = append(bases, prefix)
  896. }
  897. for _, a := range addrs {
  898. host := wireguardHostAddr(a)
  899. if !host.IsValid() {
  900. return false
  901. }
  902. fits := false
  903. for _, prefix := range bases {
  904. if prefix.Contains(host) {
  905. fits = true
  906. break
  907. }
  908. }
  909. if !fits {
  910. return false
  911. }
  912. }
  913. return true
  914. }
  915. // Attach applies the client to every requested inbound: one failing inbound no
  916. // longer aborts the others, so the error can name several and needRestart holds.
  917. func (s *ClientService) Attach(inboundSvc *InboundService, id int, inboundIds []int) (bool, error) {
  918. existing, err := s.GetByID(id)
  919. if err != nil {
  920. return false, err
  921. }
  922. currentIds, err := s.GetInboundIdsForRecord(id)
  923. if err != nil {
  924. return false, err
  925. }
  926. have := make(map[int]struct{}, len(currentIds))
  927. for _, x := range currentIds {
  928. have[x] = struct{}{}
  929. }
  930. clientWire := existing.ToClient()
  931. flow, ffErr := s.EffectiveFlow(nil, id)
  932. if ffErr != nil {
  933. return false, ffErr
  934. }
  935. clientWire.Flow = flow
  936. clientWire.UpdatedAt = time.Now().UnixMilli()
  937. // If this identity has no CURRENT WireGuard/AmneziaWG attachment,
  938. // clientWire.AllowedIPs (from the ClientRecord) is a leftover from
  939. // whenever it last had one -- nothing reserves it anymore. Clear it so
  940. // attaching to a tunnel inbound now allocates a fresh address instead
  941. // of resurrecting the old one, which may no longer even be the lowest
  942. // free slot. Left untouched when the identity already has an active
  943. // tunnel elsewhere, so extending it to a second protocol still keeps
  944. // the same address on both.
  945. if !s.hasTunnelAttachment(inboundSvc, currentIds) {
  946. clientWire.AllowedIPs = nil
  947. }
  948. adds := make([]*model.Inbound, 0, len(inboundIds))
  949. for _, ibId := range inboundIds {
  950. if _, attached := have[ibId]; attached {
  951. continue
  952. }
  953. inbound, getErr := inboundSvc.GetInbound(ibId)
  954. if getErr != nil {
  955. return false, fmt.Errorf("inbound %d: %w", ibId, getErr)
  956. }
  957. copyClient := *clientWire
  958. if !addressesFitAmneziaWGInbound(copyClient.AllowedIPs, inbound) {
  959. copyClient.AllowedIPs = nil
  960. }
  961. if err := s.fillProtocolDefaults(&copyClient, inbound); err != nil {
  962. return false, fmt.Errorf("inbound %d: %w", ibId, err)
  963. }
  964. settingsPayload, mErr := json.Marshal(map[string][]model.Client{"clients": {clientWithInboundFlow(copyClient, inbound)}})
  965. if mErr != nil {
  966. return false, fmt.Errorf("inbound %d: %w", ibId, mErr)
  967. }
  968. adds = append(adds, &model.Inbound{Id: ibId, Settings: string(settingsPayload)})
  969. }
  970. return s.fanoutInboundClientAdds(inboundSvc, adds)
  971. }
  972. func (s *ClientService) CreateOne(inboundSvc *InboundService, inboundId int, client model.Client) (bool, error) {
  973. return s.Create(inboundSvc, &ClientCreatePayload{
  974. Client: client,
  975. InboundIds: []int{inboundId},
  976. })
  977. }
  978. func (s *ClientService) DetachByEmail(inboundSvc *InboundService, inboundId int, email string) (bool, error) {
  979. if email == "" {
  980. return false, common.NewError("client email is required")
  981. }
  982. rec, err := s.GetRecordByEmail(nil, email)
  983. if err != nil {
  984. return false, err
  985. }
  986. return s.Detach(inboundSvc, rec.Id, []int{inboundId})
  987. }
  988. func (s *ClientService) AttachByEmail(inboundSvc *InboundService, email string, inboundIds []int) (bool, error) {
  989. if email == "" {
  990. return false, common.NewError("client email is required")
  991. }
  992. rec, err := s.GetRecordByEmail(nil, email)
  993. if err != nil {
  994. return false, err
  995. }
  996. return s.Attach(inboundSvc, rec.Id, inboundIds)
  997. }
  998. func (s *ClientService) DetachByEmailMany(inboundSvc *InboundService, email string, inboundIds []int) (bool, error) {
  999. if email == "" {
  1000. return false, common.NewError("client email is required")
  1001. }
  1002. rec, err := s.GetRecordByEmail(nil, email)
  1003. if err != nil {
  1004. return false, err
  1005. }
  1006. return s.Detach(inboundSvc, rec.Id, inboundIds)
  1007. }
  1008. func (s *ClientService) DeleteByEmail(inboundSvc *InboundService, email string, keepTraffic bool) (bool, error) {
  1009. if email == "" {
  1010. return false, common.NewError("client email is required")
  1011. }
  1012. rec, err := s.GetRecordByEmail(nil, email)
  1013. if err == nil {
  1014. return s.Delete(inboundSvc, rec.Id, keepTraffic)
  1015. }
  1016. if !errors.Is(err, gorm.ErrRecordNotFound) {
  1017. return false, err
  1018. }
  1019. inboundIds, idsErr := s.findInboundIdsByClientEmail(email)
  1020. if idsErr != nil {
  1021. return false, idsErr
  1022. }
  1023. if len(inboundIds) == 0 {
  1024. return false, common.NewError(fmt.Sprintf("client %q not found in any inbound or client record", email))
  1025. }
  1026. applies := make([]inboundApply, 0, len(inboundIds))
  1027. for _, ibId := range inboundIds {
  1028. applies = append(applies, inboundApply{id: ibId, run: func() (bool, error) {
  1029. nr, delErr := s.DelInboundClientByEmail(inboundSvc, ibId, email, keepTraffic, true)
  1030. if errors.Is(delErr, ErrClientNotInInbound) {
  1031. return nr, nil
  1032. }
  1033. return nr, delErr
  1034. }})
  1035. }
  1036. needRestart, delErr := fanoutInboundApplies(applies)
  1037. if delErr != nil {
  1038. return needRestart, delErr
  1039. }
  1040. if !keepTraffic {
  1041. db := database.GetDB()
  1042. if err := db.Where("email = ?", email).Delete(&xray.ClientTraffic{}).Error; err != nil {
  1043. return needRestart, err
  1044. }
  1045. if err := clearGlobalTraffic(db, email); err != nil {
  1046. return needRestart, err
  1047. }
  1048. if err := db.Where("client_email = ?", email).Delete(&model.InboundClientIps{}).Error; err != nil {
  1049. return needRestart, err
  1050. }
  1051. if err := db.Where("email = ?", email).Delete(&model.NodeClientTraffic{}).Error; err != nil {
  1052. return needRestart, err
  1053. }
  1054. }
  1055. return needRestart, nil
  1056. }
  1057. func (s *ClientService) UpdateByEmail(inboundSvc *InboundService, email string, updated model.Client, limitHwid int, inboundFilter ...int) (bool, error) {
  1058. if email == "" {
  1059. return false, common.NewError("client email is required")
  1060. }
  1061. rec, err := s.GetRecordByEmail(nil, email)
  1062. if err != nil {
  1063. return false, err
  1064. }
  1065. return s.Update(inboundSvc, rec.Id, updated, limitHwid, inboundFilter...)
  1066. }
  1067. func (s *ClientService) Detach(inboundSvc *InboundService, id int, inboundIds []int) (bool, error) {
  1068. existing, err := s.GetByID(id)
  1069. if err != nil {
  1070. return false, err
  1071. }
  1072. currentIds, err := s.GetInboundIdsForRecord(id)
  1073. if err != nil {
  1074. return false, err
  1075. }
  1076. have := make(map[int]struct{}, len(currentIds))
  1077. for _, x := range currentIds {
  1078. have[x] = struct{}{}
  1079. }
  1080. applies := make([]inboundApply, 0, len(inboundIds))
  1081. for _, ibId := range inboundIds {
  1082. if _, attached := have[ibId]; !attached {
  1083. continue
  1084. }
  1085. if _, getErr := inboundSvc.GetInbound(ibId); getErr != nil {
  1086. return false, getErr
  1087. }
  1088. // Detach by email — the client's stable identity (see Delete).
  1089. if existing.Email == "" {
  1090. continue
  1091. }
  1092. applies = append(applies, inboundApply{id: ibId, run: func() (bool, error) {
  1093. nr, delErr := s.DelInboundClientByEmail(inboundSvc, ibId, existing.Email, true, false)
  1094. if errors.Is(delErr, ErrClientNotInInbound) {
  1095. return nr, nil
  1096. }
  1097. return nr, delErr
  1098. }})
  1099. }
  1100. return fanoutInboundApplies(applies)
  1101. }