package service import ( "context" "encoding/base64" "encoding/json" "errors" "fmt" "strings" "sync" "time" "github.com/google/uuid" "github.com/mhsanaei/3x-ui/v3/database" "github.com/mhsanaei/3x-ui/v3/database/model" "github.com/mhsanaei/3x-ui/v3/logger" "github.com/mhsanaei/3x-ui/v3/util/common" "github.com/mhsanaei/3x-ui/v3/util/random" "github.com/mhsanaei/3x-ui/v3/xray" "gorm.io/gorm" ) type ClientWithAttachments struct { model.ClientRecord InboundIds []int `json:"inboundIds"` Traffic *xray.ClientTraffic `json:"traffic,omitempty"` } // MarshalJSON is required because model.ClientRecord defines its own // MarshalJSON. Go promotes the embedded method to the outer struct, so without // this the encoder would call ClientRecord.MarshalJSON for the whole value and // silently drop InboundIds and Traffic from the API response. func (c ClientWithAttachments) MarshalJSON() ([]byte, error) { rec, err := json.Marshal(c.ClientRecord) if err != nil { return nil, err } extras := struct { InboundIds []int `json:"inboundIds"` Traffic *xray.ClientTraffic `json:"traffic,omitempty"` }{InboundIds: c.InboundIds, Traffic: c.Traffic} extra, err := json.Marshal(extras) if err != nil { return nil, err } if len(rec) < 2 || rec[len(rec)-1] != '}' || len(extra) <= 2 { return rec, nil } const maxMarshalSize = 256 << 20 if len(rec) > maxMarshalSize || len(extra) > maxMarshalSize { return rec, nil } out := make([]byte, 0, len(rec)+len(extra)) out = append(out, rec[:len(rec)-1]...) if len(rec) > 2 { out = append(out, ',') } out = append(out, extra[1:]...) return out, nil } func clientKeyForProtocol(p model.Protocol, rec *model.ClientRecord) string { if rec == nil { return "" } switch p { case model.Trojan: return rec.Password case model.Shadowsocks: return rec.Email case model.Hysteria, model.Hysteria2: return rec.Auth default: return rec.UUID } } type ClientService struct{} // Short-lived tombstone of just-deleted client emails so that a node snapshot // arriving between delete and node-side processing doesn't resurrect them. var ( recentlyDeletedMu sync.Mutex recentlyDeleted = map[string]time.Time{} ) const deleteTombstoneTTL = 90 * time.Second var ( inboundMutationLocksMu sync.Mutex inboundMutationLocks = map[int]*sync.Mutex{} ) func lockInbound(inboundId int) *sync.Mutex { inboundMutationLocksMu.Lock() defer inboundMutationLocksMu.Unlock() m, ok := inboundMutationLocks[inboundId] if !ok { m = &sync.Mutex{} inboundMutationLocks[inboundId] = m } m.Lock() return m } func compactOrphans(db *gorm.DB, clients []any) []any { if len(clients) == 0 { return clients } emails := make([]string, 0, len(clients)) for _, c := range clients { cm, ok := c.(map[string]any) if !ok { continue } if e, _ := cm["email"].(string); e != "" { emails = append(emails, e) } } if len(emails) == 0 { return clients } var existingEmails []string if err := db.Model(&model.ClientRecord{}).Where("email IN ?", emails).Pluck("email", &existingEmails).Error; err != nil { logger.Warning("compactOrphans pluck:", err) return clients } if len(existingEmails) == len(emails) { return clients } existing := make(map[string]struct{}, len(existingEmails)) for _, e := range existingEmails { existing[e] = struct{}{} } out := make([]any, 0, len(existingEmails)) for _, c := range clients { cm, ok := c.(map[string]any) if !ok { out = append(out, c) continue } e, _ := cm["email"].(string) if e == "" { out = append(out, c) continue } if _, ok := existing[e]; ok { out = append(out, c) } } return out } func tombstoneClientEmail(email string) { if email == "" { return } recentlyDeletedMu.Lock() defer recentlyDeletedMu.Unlock() recentlyDeleted[email] = time.Now() cutoff := time.Now().Add(-deleteTombstoneTTL) for e, ts := range recentlyDeleted { if ts.Before(cutoff) { delete(recentlyDeleted, e) } } } func isClientEmailTombstoned(email string) bool { if email == "" { return false } recentlyDeletedMu.Lock() defer recentlyDeletedMu.Unlock() ts, ok := recentlyDeleted[email] if !ok { return false } if time.Since(ts) > deleteTombstoneTTL { delete(recentlyDeleted, email) return false } return true } func (s *ClientService) SyncInbound(tx *gorm.DB, inboundId int, clients []model.Client) error { if tx == nil { tx = database.GetDB() } if err := tx.Where("inbound_id = ?", inboundId).Delete(&model.ClientInbound{}).Error; err != nil { return err } for i := range clients { c := clients[i] email := strings.TrimSpace(c.Email) if email == "" { continue } incoming := c.ToRecord() row := &model.ClientRecord{} err := tx.Where("email = ?", email).First(row).Error if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } if errors.Is(err, gorm.ErrRecordNotFound) { if isClientEmailTombstoned(email) { continue } if err := tx.Create(incoming).Error; err != nil { return err } row = incoming } else { row.UUID = incoming.UUID row.Password = incoming.Password row.Auth = incoming.Auth row.Flow = incoming.Flow row.Security = incoming.Security row.Reverse = incoming.Reverse row.SubID = incoming.SubID row.LimitIP = incoming.LimitIP row.TotalGB = incoming.TotalGB row.ExpiryTime = incoming.ExpiryTime row.Enable = incoming.Enable row.TgID = incoming.TgID row.Comment = incoming.Comment row.Reset = incoming.Reset if incoming.CreatedAt > 0 && (row.CreatedAt == 0 || incoming.CreatedAt < row.CreatedAt) { row.CreatedAt = incoming.CreatedAt } if incoming.UpdatedAt > row.UpdatedAt { row.UpdatedAt = incoming.UpdatedAt } if err := tx.Save(row).Error; err != nil { return err } } link := model.ClientInbound{ ClientId: row.Id, InboundId: inboundId, FlowOverride: c.Flow, } if err := tx.Create(&link).Error; err != nil { return err } } return nil } func (s *ClientService) DetachInbound(tx *gorm.DB, inboundId int) error { if tx == nil { tx = database.GetDB() } return tx.Where("inbound_id = ?", inboundId).Delete(&model.ClientInbound{}).Error } func (s *ClientService) ListForInbound(tx *gorm.DB, inboundId int) ([]model.Client, error) { if tx == nil { tx = database.GetDB() } type joinedRow struct { model.ClientRecord FlowOverride string } var rows []joinedRow err := tx.Table("clients"). Select("clients.*, client_inbounds.flow_override AS flow_override"). Joins("JOIN client_inbounds ON client_inbounds.client_id = clients.id"). Where("client_inbounds.inbound_id = ?", inboundId). Order("clients.id ASC"). Find(&rows).Error if err != nil { return nil, err } out := make([]model.Client, 0, len(rows)) for i := range rows { c := rows[i].ToClient() if rows[i].FlowOverride != "" { c.Flow = rows[i].FlowOverride } out = append(out, *c) } return out, nil } func (s *ClientService) GetRecordByEmail(tx *gorm.DB, email string) (*model.ClientRecord, error) { if tx == nil { tx = database.GetDB() } row := &model.ClientRecord{} err := tx.Where("email = ?", email).First(row).Error if err != nil { return nil, err } return row, nil } func (s *ClientService) GetInboundIdsForEmail(tx *gorm.DB, email string) ([]int, error) { if tx == nil { tx = database.GetDB() } var ids []int err := tx.Table("client_inbounds"). Select("client_inbounds.inbound_id"). Joins("JOIN clients ON clients.id = client_inbounds.client_id"). Where("clients.email = ?", email). Scan(&ids).Error if err != nil { return nil, err } return ids, nil } func (s *ClientService) GetByID(id int) (*model.ClientRecord, error) { row := &model.ClientRecord{} if err := database.GetDB().Where("id = ?", id).First(row).Error; err != nil { return nil, err } return row, nil } func (s *ClientService) GetInboundIdsForRecord(id int) ([]int, error) { var ids []int err := database.GetDB().Table("client_inbounds"). Where("client_id = ?", id). Order("inbound_id ASC"). Pluck("inbound_id", &ids).Error if err != nil { return nil, err } return ids, nil } func (s *ClientService) List() ([]ClientWithAttachments, error) { db := database.GetDB() var rows []model.ClientRecord if err := db.Order("id ASC").Find(&rows).Error; err != nil { return nil, err } if len(rows) == 0 { return []ClientWithAttachments{}, nil } clientIds := make([]int, 0, len(rows)) emails := make([]string, 0, len(rows)) for i := range rows { clientIds = append(clientIds, rows[i].Id) if rows[i].Email != "" { emails = append(emails, rows[i].Email) } } var links []model.ClientInbound if err := db.Where("client_id IN ?", clientIds).Find(&links).Error; err != nil { return nil, err } attachments := make(map[int][]int, len(rows)) for _, l := range links { attachments[l.ClientId] = append(attachments[l.ClientId], l.InboundId) } trafficByEmail := make(map[string]*xray.ClientTraffic, len(emails)) if len(emails) > 0 { var stats []xray.ClientTraffic if err := db.Where("email IN ?", emails).Find(&stats).Error; err != nil { return nil, err } for i := range stats { trafficByEmail[stats[i].Email] = &stats[i] } } out := make([]ClientWithAttachments, 0, len(rows)) for i := range rows { out = append(out, ClientWithAttachments{ ClientRecord: rows[i], InboundIds: attachments[rows[i].Id], Traffic: trafficByEmail[rows[i].Email], }) } return out, nil } type ClientCreatePayload struct { Client model.Client `json:"client"` InboundIds []int `json:"inboundIds"` } func (s *ClientService) Create(inboundSvc *InboundService, payload *ClientCreatePayload) (bool, error) { if payload == nil { return false, common.NewError("empty payload") } client := payload.Client if strings.TrimSpace(client.Email) == "" { return false, common.NewError("client email is required") } if len(payload.InboundIds) == 0 { return false, common.NewError("at least one inbound is required") } if client.SubID == "" { client.SubID = uuid.NewString() } if !client.Enable { client.Enable = true } now := time.Now().UnixMilli() if client.CreatedAt == 0 { client.CreatedAt = now } client.UpdatedAt = now existing := &model.ClientRecord{} err := database.GetDB().Where("email = ?", client.Email).First(existing).Error if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return false, err } emailTaken := !errors.Is(err, gorm.ErrRecordNotFound) if emailTaken { if existing.SubID == "" || existing.SubID != client.SubID { return false, common.NewError("email already in use:", client.Email) } } needRestart := false for _, ibId := range payload.InboundIds { inbound, getErr := inboundSvc.GetInbound(ibId) if getErr != nil { return needRestart, getErr } if err := s.fillProtocolDefaults(&client, inbound); err != nil { return needRestart, err } settingsPayload, mErr := json.Marshal(map[string][]model.Client{"clients": {client}}) if mErr != nil { return needRestart, mErr } nr, addErr := s.AddInboundClient(inboundSvc, &model.Inbound{ Id: ibId, Settings: string(settingsPayload), }) if addErr != nil { return needRestart, addErr } if nr { needRestart = true } } return needRestart, nil } func (s *ClientService) fillProtocolDefaults(c *model.Client, ib *model.Inbound) error { switch ib.Protocol { case model.VMESS, model.VLESS: if c.ID == "" { c.ID = uuid.NewString() } case model.Trojan: if c.Password == "" { c.Password = strings.ReplaceAll(uuid.NewString(), "-", "") } case model.Shadowsocks: method := shadowsocksMethodFromSettings(ib.Settings) if c.Password == "" || !validShadowsocksClientKey(method, c.Password) { c.Password = randomShadowsocksClientKey(method) } case model.Hysteria, model.Hysteria2: if c.Auth == "" { c.Auth = strings.ReplaceAll(uuid.NewString(), "-", "") } } return nil } // shadowsocksMethodFromSettings pulls the "method" field out of the inbound's // settings JSON. Returns "" when the field is missing or settings is invalid. func shadowsocksMethodFromSettings(settings string) string { if settings == "" { return "" } var m map[string]any if err := json.Unmarshal([]byte(settings), &m); err != nil { return "" } method, _ := m["method"].(string) return method } // randomShadowsocksClientKey returns a per-client key sized to the cipher. // The 2022-blake3 ciphers require a base64-encoded key of an exact byte // length (16 bytes for aes-128-gcm, 32 bytes for aes-256-gcm and // chacha20-poly1305) — anything else fails with "bad key" on xray start. // Older ciphers accept arbitrary passwords, so we keep the uuid-style. func randomShadowsocksClientKey(method string) string { if n := shadowsocksKeyBytes(method); n > 0 { return random.Base64Bytes(n) } return strings.ReplaceAll(uuid.NewString(), "-", "") } // validShadowsocksClientKey reports whether key is acceptable for the cipher. // For 2022-blake3 it must decode to the exact byte length the cipher needs; // any other method accepts any non-empty string. func validShadowsocksClientKey(method, key string) bool { n := shadowsocksKeyBytes(method) if n == 0 { return key != "" } decoded, err := base64.StdEncoding.DecodeString(key) if err != nil { return false } return len(decoded) == n } func shadowsocksKeyBytes(method string) int { switch method { case "2022-blake3-aes-128-gcm": return 16 case "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305": return 32 } return 0 } // applyShadowsocksClientMethod ensures each client entry carries a "method" // field for legacy shadowsocks ciphers. xray's multi-user shadowsocks code // requires a per-client method; an empty/missing field fails with // "unsupported cipher method:". 2022-blake3 ciphers use the top-level // method only, so the per-client field must stay absent. func applyShadowsocksClientMethod(clients []any, settings map[string]any) { method, _ := settings["method"].(string) if method == "" || strings.HasPrefix(method, "2022-blake3-") { return } for i := range clients { cm, ok := clients[i].(map[string]any) if !ok { continue } if existing, _ := cm["method"].(string); existing != "" { continue } cm["method"] = method clients[i] = cm } } func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model.Client) (bool, error) { existing, err := s.GetByID(id) if err != nil { return false, err } inboundIds, err := s.GetInboundIdsForRecord(id) if err != nil { return false, err } if strings.TrimSpace(updated.Email) == "" { return false, common.NewError("client email is required") } if updated.SubID == "" { updated.SubID = existing.SubID } if updated.SubID == "" { updated.SubID = uuid.NewString() } updated.UpdatedAt = time.Now().UnixMilli() if updated.CreatedAt == 0 { updated.CreatedAt = existing.CreatedAt } needRestart := false for _, ibId := range inboundIds { inbound, getErr := inboundSvc.GetInbound(ibId) if getErr != nil { return needRestart, getErr } oldKey := clientKeyForProtocol(inbound.Protocol, existing) if oldKey == "" { continue } if err := s.fillProtocolDefaults(&updated, inbound); err != nil { return needRestart, err } settingsPayload, mErr := json.Marshal(map[string][]model.Client{"clients": {updated}}) if mErr != nil { return needRestart, mErr } nr, upErr := s.UpdateInboundClient(inboundSvc, &model.Inbound{ Id: ibId, Settings: string(settingsPayload), }, oldKey) if upErr != nil { return needRestart, upErr } if nr { needRestart = true } } return needRestart, nil } func (s *ClientService) Delete(inboundSvc *InboundService, id int, keepTraffic bool) (bool, error) { existing, err := s.GetByID(id) if err != nil { return false, err } tombstoneClientEmail(existing.Email) inboundIds, err := s.GetInboundIdsForRecord(id) if err != nil { return false, err } needRestart := false for _, ibId := range inboundIds { inbound, getErr := inboundSvc.GetInbound(ibId) if getErr != nil { return needRestart, getErr } key := clientKeyForProtocol(inbound.Protocol, existing) if key == "" { continue } nr, delErr := s.DelInboundClient(inboundSvc, ibId, key) if delErr != nil { return needRestart, delErr } if nr { needRestart = true } } db := database.GetDB() if err := db.Where("client_id = ?", id).Delete(&model.ClientInbound{}).Error; err != nil { return needRestart, err } if !keepTraffic && existing.Email != "" { if err := db.Where("email = ?", existing.Email).Delete(&xray.ClientTraffic{}).Error; err != nil { return needRestart, err } if err := db.Where("client_email = ?", existing.Email).Delete(&model.InboundClientIps{}).Error; err != nil { return needRestart, err } } if err := db.Delete(&model.ClientRecord{}, id).Error; err != nil { return needRestart, err } return needRestart, nil } func (s *ClientService) Attach(inboundSvc *InboundService, id int, inboundIds []int) (bool, error) { existing, err := s.GetByID(id) if err != nil { return false, err } currentIds, err := s.GetInboundIdsForRecord(id) if err != nil { return false, err } have := make(map[int]struct{}, len(currentIds)) for _, x := range currentIds { have[x] = struct{}{} } clientWire := existing.ToClient() clientWire.UpdatedAt = time.Now().UnixMilli() needRestart := false for _, ibId := range inboundIds { if _, attached := have[ibId]; attached { continue } inbound, getErr := inboundSvc.GetInbound(ibId) if getErr != nil { return needRestart, getErr } copyClient := *clientWire if err := s.fillProtocolDefaults(©Client, inbound); err != nil { return needRestart, err } settingsPayload, mErr := json.Marshal(map[string][]model.Client{"clients": {copyClient}}) if mErr != nil { return needRestart, mErr } nr, addErr := s.AddInboundClient(inboundSvc, &model.Inbound{ Id: ibId, Settings: string(settingsPayload), }) if addErr != nil { return needRestart, addErr } if nr { needRestart = true } } return needRestart, nil } func (s *ClientService) CreateOne(inboundSvc *InboundService, inboundId int, client model.Client) (bool, error) { return s.Create(inboundSvc, &ClientCreatePayload{ Client: client, InboundIds: []int{inboundId}, }) } func (s *ClientService) DetachByEmail(inboundSvc *InboundService, inboundId int, email string) (bool, error) { if email == "" { return false, common.NewError("client email is required") } rec, err := s.GetRecordByEmail(nil, email) if err != nil { return false, err } return s.Detach(inboundSvc, rec.Id, []int{inboundId}) } func (s *ClientService) AttachByEmail(inboundSvc *InboundService, email string, inboundIds []int) (bool, error) { if email == "" { return false, common.NewError("client email is required") } rec, err := s.GetRecordByEmail(nil, email) if err != nil { return false, err } return s.Attach(inboundSvc, rec.Id, inboundIds) } func (s *ClientService) DetachByEmailMany(inboundSvc *InboundService, email string, inboundIds []int) (bool, error) { if email == "" { return false, common.NewError("client email is required") } rec, err := s.GetRecordByEmail(nil, email) if err != nil { return false, err } return s.Detach(inboundSvc, rec.Id, inboundIds) } func (s *ClientService) DeleteByEmail(inboundSvc *InboundService, email string, keepTraffic bool) (bool, error) { if email == "" { return false, common.NewError("client email is required") } rec, err := s.GetRecordByEmail(nil, email) if err != nil { return false, err } return s.Delete(inboundSvc, rec.Id, keepTraffic) } func (s *ClientService) UpdateByEmail(inboundSvc *InboundService, email string, updated model.Client) (bool, error) { if email == "" { return false, common.NewError("client email is required") } rec, err := s.GetRecordByEmail(nil, email) if err != nil { return false, err } return s.Update(inboundSvc, rec.Id, updated) } func (s *ClientService) ResetTrafficByEmail(inboundSvc *InboundService, email string) (bool, error) { if email == "" { return false, common.NewError("client email is required") } rec, err := s.GetRecordByEmail(nil, email) if err != nil { return false, err } inboundIds, err := s.GetInboundIdsForRecord(rec.Id) if err != nil { return false, err } if len(inboundIds) == 0 { if rErr := inboundSvc.ResetClientTrafficByEmail(email); rErr != nil { return false, rErr } return false, nil } needRestart := false for _, ibId := range inboundIds { nr, rErr := inboundSvc.ResetClientTraffic(ibId, email) if rErr != nil { return needRestart, rErr } if nr { needRestart = true } } return needRestart, nil } func (s *ClientService) DelDepleted(inboundSvc *InboundService) (int, bool, error) { db := database.GetDB() now := time.Now().UnixMilli() depletedClause := "reset = 0 and ((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?))" var rows []xray.ClientTraffic if err := db.Where(depletedClause, now).Find(&rows).Error; err != nil { return 0, false, err } if len(rows) == 0 { return 0, false, nil } emails := make(map[string]struct{}, len(rows)) for _, r := range rows { if r.Email != "" { emails[r.Email] = struct{}{} } } needRestart := false deleted := 0 for email := range emails { var rec model.ClientRecord if err := db.Where("email = ?", email).First(&rec).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { continue } return deleted, needRestart, err } nr, err := s.Delete(inboundSvc, rec.Id, false) if err != nil { return deleted, needRestart, err } if nr { needRestart = true } deleted++ } return deleted, needRestart, nil } func (s *ClientService) ResetAllClientTraffics(inboundSvc *InboundService, id int) error { return submitTrafficWrite(func() error { return s.resetAllClientTrafficsLocked(id) }) } func (s *ClientService) resetAllClientTrafficsLocked(id int) error { db := database.GetDB() now := time.Now().Unix() * 1000 if err := db.Transaction(func(tx *gorm.DB) error { whereText := "inbound_id " if id == -1 { whereText += " > ?" } else { whereText += " = ?" } result := tx.Model(xray.ClientTraffic{}). Where(whereText, id). Updates(map[string]any{"enable": true, "up": 0, "down": 0}) if result.Error != nil { return result.Error } inboundWhereText := "id " if id == -1 { inboundWhereText += " > ?" } else { inboundWhereText += " = ?" } result = tx.Model(model.Inbound{}). Where(inboundWhereText, id). Update("last_traffic_reset_time", now) return result.Error }); err != nil { return err } return nil } func (s *ClientService) ResetAllTraffics() (bool, error) { res := database.GetDB().Model(&xray.ClientTraffic{}). Where("1 = 1"). Updates(map[string]any{"up": 0, "down": 0}) if res.Error != nil { return false, res.Error } return res.RowsAffected > 0, nil } func (s *ClientService) Detach(inboundSvc *InboundService, id int, inboundIds []int) (bool, error) { existing, err := s.GetByID(id) if err != nil { return false, err } currentIds, err := s.GetInboundIdsForRecord(id) if err != nil { return false, err } have := make(map[int]struct{}, len(currentIds)) for _, x := range currentIds { have[x] = struct{}{} } needRestart := false for _, ibId := range inboundIds { if _, attached := have[ibId]; !attached { continue } inbound, getErr := inboundSvc.GetInbound(ibId) if getErr != nil { return needRestart, getErr } key := clientKeyForProtocol(inbound.Protocol, existing) if key == "" { continue } nr, delErr := s.DelInboundClient(inboundSvc, ibId, key) if delErr != nil { return needRestart, delErr } if nr { needRestart = true } } return needRestart, nil } func (s *ClientService) checkEmailsExistForClients(inboundSvc *InboundService, clients []model.Client) (string, error) { emailSubIDs, err := inboundSvc.getAllEmailSubIDs() if err != nil { return "", err } seen := make(map[string]string, len(clients)) for _, client := range clients { if client.Email == "" { continue } key := strings.ToLower(client.Email) if prev, ok := seen[key]; ok { if prev != client.SubID || client.SubID == "" { return client.Email, nil } continue } seen[key] = client.SubID if existingSub, ok := emailSubIDs[key]; ok { if client.SubID == "" || existingSub == "" || existingSub != client.SubID { return client.Email, nil } } } return "", nil } func (s *ClientService) AddInboundClient(inboundSvc *InboundService, data *model.Inbound) (bool, error) { defer lockInbound(data.Id).Unlock() clients, err := inboundSvc.GetClients(data) if err != nil { return false, err } var settings map[string]any err = json.Unmarshal([]byte(data.Settings), &settings) if err != nil { return false, err } interfaceClients := settings["clients"].([]any) nowTs := time.Now().Unix() * 1000 for i := range interfaceClients { if cm, ok := interfaceClients[i].(map[string]any); ok { if _, ok2 := cm["created_at"]; !ok2 { cm["created_at"] = nowTs } cm["updated_at"] = nowTs interfaceClients[i] = cm } } existEmail, err := s.checkEmailsExistForClients(inboundSvc, clients) if err != nil { return false, err } if existEmail != "" { return false, common.NewError("Duplicate email:", existEmail) } oldInbound, err := inboundSvc.GetInbound(data.Id) if err != nil { return false, err } for _, client := range clients { if strings.TrimSpace(client.Email) == "" { return false, common.NewError("client email is required") } switch oldInbound.Protocol { case "trojan": if client.Password == "" { return false, common.NewError("empty client ID") } case "shadowsocks": if client.Email == "" { return false, common.NewError("empty client ID") } case "hysteria", "hysteria2": if client.Auth == "" { return false, common.NewError("empty client ID") } default: if client.ID == "" { return false, common.NewError("empty client ID") } } } var oldSettings map[string]any err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) if err != nil { return false, err } if oldInbound.Protocol == model.Shadowsocks { applyShadowsocksClientMethod(interfaceClients, oldSettings) } oldClients := oldSettings["clients"].([]any) oldClients = compactOrphans(database.GetDB(), oldClients) oldClients = append(oldClients, interfaceClients...) oldSettings["clients"] = oldClients newSettings, err := json.MarshalIndent(oldSettings, "", " ") if err != nil { return false, err } oldInbound.Settings = string(newSettings) db := database.GetDB() tx := db.Begin() defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() needRestart := false rt, rterr := inboundSvc.runtimeFor(oldInbound) if rterr != nil { if oldInbound.NodeID != nil { err = rterr return false, err } needRestart = true } else if oldInbound.NodeID == nil { for _, client := range clients { if len(client.Email) == 0 { needRestart = true continue } inboundSvc.AddClientStat(tx, data.Id, &client) if !client.Enable { continue } cipher := "" if oldInbound.Protocol == "shadowsocks" { cipher = oldSettings["method"].(string) } err1 := rt.AddUser(context.Background(), oldInbound, map[string]any{ "email": client.Email, "id": client.ID, "auth": client.Auth, "security": client.Security, "flow": client.Flow, "password": client.Password, "cipher": cipher, }) if err1 == nil { logger.Debug("Client added on", rt.Name(), ":", client.Email) } else { logger.Debug("Error in adding client on", rt.Name(), ":", err1) needRestart = true } } } else { for _, client := range clients { if len(client.Email) > 0 { inboundSvc.AddClientStat(tx, data.Id, &client) } if err1 := rt.AddClient(context.Background(), oldInbound, client); err1 != nil { err = err1 return false, err } } } if err = tx.Save(oldInbound).Error; err != nil { return false, err } finalClients, gcErr := inboundSvc.GetClients(oldInbound) if gcErr != nil { err = gcErr return false, err } if err = s.SyncInbound(tx, oldInbound.Id, finalClients); err != nil { return false, err } return needRestart, nil } func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *model.Inbound, clientId string) (bool, error) { defer lockInbound(data.Id).Unlock() clients, err := inboundSvc.GetClients(data) if err != nil { return false, err } var settings map[string]any err = json.Unmarshal([]byte(data.Settings), &settings) if err != nil { return false, err } interfaceClients := settings["clients"].([]any) oldInbound, err := inboundSvc.GetInbound(data.Id) if err != nil { return false, err } oldClients, err := inboundSvc.GetClients(oldInbound) if err != nil { return false, err } oldEmail := "" newClientId := "" clientIndex := -1 for index, oldClient := range oldClients { oldClientId := "" switch oldInbound.Protocol { case "trojan": oldClientId = oldClient.Password newClientId = clients[0].Password case "shadowsocks": oldClientId = oldClient.Email newClientId = clients[0].Email case "hysteria", "hysteria2": oldClientId = oldClient.Auth newClientId = clients[0].Auth default: oldClientId = oldClient.ID newClientId = clients[0].ID } if clientId == oldClientId { oldEmail = oldClient.Email clientIndex = index break } } if newClientId == "" || clientIndex == -1 { return false, common.NewError("empty client ID") } if strings.TrimSpace(clients[0].Email) == "" { return false, common.NewError("client email is required") } if clients[0].Email != oldEmail { existEmail, err := s.checkEmailsExistForClients(inboundSvc, clients) if err != nil { return false, err } if existEmail != "" { return false, common.NewError("Duplicate email:", existEmail) } } var oldSettings map[string]any err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) if err != nil { return false, err } settingsClients := oldSettings["clients"].([]any) var preservedCreated any if clientIndex >= 0 && clientIndex < len(settingsClients) { if oldMap, ok := settingsClients[clientIndex].(map[string]any); ok { if v, ok2 := oldMap["created_at"]; ok2 { preservedCreated = v } } } if len(interfaceClients) > 0 { if newMap, ok := interfaceClients[0].(map[string]any); ok { if preservedCreated == nil { preservedCreated = time.Now().Unix() * 1000 } newMap["created_at"] = preservedCreated newMap["updated_at"] = time.Now().Unix() * 1000 interfaceClients[0] = newMap } } if oldInbound.Protocol == model.Shadowsocks { applyShadowsocksClientMethod(interfaceClients, oldSettings) } settingsClients[clientIndex] = interfaceClients[0] oldSettings["clients"] = settingsClients if oldInbound.Protocol == model.VLESS { hasVisionFlow := false for _, c := range settingsClients { cm, ok := c.(map[string]any) if !ok { continue } if flow, _ := cm["flow"].(string); flow == "xtls-rprx-vision" { hasVisionFlow = true break } } if !hasVisionFlow { delete(oldSettings, "testseed") } } newSettings, err := json.MarshalIndent(oldSettings, "", " ") if err != nil { return false, err } oldInbound.Settings = string(newSettings) db := database.GetDB() tx := db.Begin() defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() if len(clients[0].Email) > 0 { if len(oldEmail) > 0 { emailUnchanged := strings.EqualFold(oldEmail, clients[0].Email) targetExists := int64(0) if !emailUnchanged { if err = tx.Model(xray.ClientTraffic{}).Where("email = ?", clients[0].Email).Count(&targetExists).Error; err != nil { return false, err } } if emailUnchanged || targetExists == 0 { err = inboundSvc.UpdateClientStat(tx, oldEmail, &clients[0]) if err != nil { return false, err } err = inboundSvc.UpdateClientIPs(tx, oldEmail, clients[0].Email) if err != nil { return false, err } } else { stillUsed, sErr := inboundSvc.emailUsedByOtherInbounds(oldEmail, data.Id) if sErr != nil { return false, sErr } if !stillUsed { if err = inboundSvc.DelClientStat(tx, oldEmail); err != nil { return false, err } if err = inboundSvc.DelClientIPs(tx, oldEmail); err != nil { return false, err } } if err = inboundSvc.UpdateClientStat(tx, clients[0].Email, &clients[0]); err != nil { return false, err } } } else { inboundSvc.AddClientStat(tx, data.Id, &clients[0]) } } else { stillUsed, err := inboundSvc.emailUsedByOtherInbounds(oldEmail, data.Id) if err != nil { return false, err } if !stillUsed { err = inboundSvc.DelClientStat(tx, oldEmail) if err != nil { return false, err } err = inboundSvc.DelClientIPs(tx, oldEmail) if err != nil { return false, err } } } needRestart := false if len(oldEmail) > 0 { rt, rterr := inboundSvc.runtimeFor(oldInbound) if rterr != nil { if oldInbound.NodeID != nil { err = rterr return false, err } needRestart = true } else if oldInbound.NodeID == nil { if oldClients[clientIndex].Enable { err1 := rt.RemoveUser(context.Background(), oldInbound, oldEmail) if err1 == nil { logger.Debug("Old client deleted on", rt.Name(), ":", oldEmail) } else if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", oldEmail)) { logger.Debug("User is already deleted. Nothing to do more...") } else { logger.Debug("Error in deleting client on", rt.Name(), ":", err1) needRestart = true } } if clients[0].Enable { cipher := "" if oldInbound.Protocol == "shadowsocks" { cipher = oldSettings["method"].(string) } err1 := rt.AddUser(context.Background(), oldInbound, map[string]any{ "email": clients[0].Email, "id": clients[0].ID, "security": clients[0].Security, "flow": clients[0].Flow, "auth": clients[0].Auth, "password": clients[0].Password, "cipher": cipher, }) if err1 == nil { logger.Debug("Client edited on", rt.Name(), ":", clients[0].Email) } else { logger.Debug("Error in adding client on", rt.Name(), ":", err1) needRestart = true } } } else { if err1 := rt.UpdateUser(context.Background(), oldInbound, oldEmail, clients[0]); err1 != nil { err = err1 return false, err } } } else { logger.Debug("Client old email not found") needRestart = true } if err = tx.Save(oldInbound).Error; err != nil { return false, err } finalClients, gcErr := inboundSvc.GetClients(oldInbound) if gcErr != nil { err = gcErr return false, err } if err = s.SyncInbound(tx, oldInbound.Id, finalClients); err != nil { return false, err } return needRestart, nil } func (s *ClientService) DelInboundClient(inboundSvc *InboundService, inboundId int, clientId string) (bool, error) { defer lockInbound(inboundId).Unlock() oldInbound, err := inboundSvc.GetInbound(inboundId) if err != nil { logger.Error("Load Old Data Error") return false, err } var settings map[string]any err = json.Unmarshal([]byte(oldInbound.Settings), &settings) if err != nil { return false, err } email := "" client_key := "id" switch oldInbound.Protocol { case "trojan": client_key = "password" case "shadowsocks": client_key = "email" case "hysteria", "hysteria2": client_key = "auth" } interfaceClients := settings["clients"].([]any) var newClients []any needApiDel := false clientFound := false for _, client := range interfaceClients { c := client.(map[string]any) c_id := c[client_key].(string) if c_id == clientId { clientFound = true email, _ = c["email"].(string) needApiDel, _ = c["enable"].(bool) } else { newClients = append(newClients, client) } } if !clientFound { return false, common.NewError("Client Not Found In Inbound For ID:", clientId) } db := database.GetDB() newClients = compactOrphans(db, newClients) if newClients == nil { newClients = []any{} } settings["clients"] = newClients newSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, err } oldInbound.Settings = string(newSettings) emailShared, err := inboundSvc.emailUsedByOtherInbounds(email, inboundId) if err != nil { return false, err } if !emailShared { err = inboundSvc.DelClientIPs(db, email) if err != nil { logger.Error("Error in delete client IPs") return false, err } } needRestart := false if len(email) > 0 { var enables []bool err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Limit(1).Pluck("enable", &enables).Error if err != nil { logger.Error("Get stats error") return false, err } notDepleted := len(enables) > 0 && enables[0] if !emailShared { err = inboundSvc.DelClientStat(db, email) if err != nil { logger.Error("Delete stats Data Error") return false, err } } if needApiDel && notDepleted && oldInbound.NodeID == nil { rt, rterr := inboundSvc.runtimeFor(oldInbound) if rterr != nil { needRestart = true } else { err1 := rt.RemoveUser(context.Background(), oldInbound, email) if err1 == nil { logger.Debug("Client deleted on", rt.Name(), ":", email) needRestart = false } else if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", email)) { logger.Debug("User is already deleted. Nothing to do more...") } else { logger.Debug("Error in deleting client on", rt.Name(), ":", err1) needRestart = true } } } } if oldInbound.NodeID != nil && len(email) > 0 { rt, rterr := inboundSvc.runtimeFor(oldInbound) if rterr != nil { return false, rterr } if err1 := rt.DeleteUser(context.Background(), oldInbound, email); err1 != nil { return false, err1 } } if err := db.Save(oldInbound).Error; err != nil { return false, err } finalClients, gcErr := inboundSvc.GetClients(oldInbound) if gcErr != nil { return false, gcErr } if err := s.SyncInbound(db, inboundId, finalClients); err != nil { return false, err } return needRestart, nil } func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inboundId int, email string) (bool, error) { defer lockInbound(inboundId).Unlock() oldInbound, err := inboundSvc.GetInbound(inboundId) if err != nil { logger.Error("Load Old Data Error") return false, err } var settings map[string]any if err := json.Unmarshal([]byte(oldInbound.Settings), &settings); err != nil { return false, err } interfaceClients, ok := settings["clients"].([]any) if !ok { return false, common.NewError("invalid clients format in inbound settings") } var newClients []any needApiDel := false found := false for _, client := range interfaceClients { c, ok := client.(map[string]any) if !ok { continue } if cEmail, ok := c["email"].(string); ok && cEmail == email { found = true needApiDel, _ = c["enable"].(bool) } else { newClients = append(newClients, client) } } if !found { return false, common.NewError(fmt.Sprintf("client with email %s not found", email)) } db := database.GetDB() newClients = compactOrphans(db, newClients) if newClients == nil { newClients = []any{} } settings["clients"] = newClients newSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, err } oldInbound.Settings = string(newSettings) emailShared, err := inboundSvc.emailUsedByOtherInbounds(email, inboundId) if err != nil { return false, err } if !emailShared { if err := inboundSvc.DelClientIPs(db, email); err != nil { logger.Error("Error in delete client IPs") return false, err } } needRestart := false if len(email) > 0 && !emailShared { traffic, err := inboundSvc.GetClientTrafficByEmail(email) if err != nil { return false, err } if traffic != nil { if err := inboundSvc.DelClientStat(db, email); err != nil { logger.Error("Delete stats Data Error") return false, err } } if needApiDel { rt, rterr := inboundSvc.runtimeFor(oldInbound) if rterr != nil { if oldInbound.NodeID != nil { return false, rterr } needRestart = true } else if oldInbound.NodeID == nil { if err1 := rt.RemoveUser(context.Background(), oldInbound, email); err1 == nil { logger.Debug("Client deleted on", rt.Name(), ":", email) needRestart = false } else if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", email)) { logger.Debug("User is already deleted. Nothing to do more...") } else { logger.Debug("Error in deleting client on", rt.Name(), ":", err1) needRestart = true } } else { if err1 := rt.DeleteUser(context.Background(), oldInbound, email); err1 != nil { return false, err1 } } } } if err := db.Save(oldInbound).Error; err != nil { return false, err } finalClients, gcErr := inboundSvc.GetClients(oldInbound) if gcErr != nil { return false, gcErr } if err := s.SyncInbound(db, inboundId, finalClients); err != nil { return false, err } return needRestart, nil } func (s *ClientService) SetClientTelegramUserID(inboundSvc *InboundService, trafficId int, tgId int64) (bool, error) { traffic, inbound, err := inboundSvc.GetClientInboundByTrafficID(trafficId) if err != nil { return false, err } if inbound == nil { return false, common.NewError("Inbound Not Found For Traffic ID:", trafficId) } clientEmail := traffic.Email oldClients, err := inboundSvc.GetClients(inbound) if err != nil { return false, err } clientId := "" for _, oldClient := range oldClients { if oldClient.Email == clientEmail { switch inbound.Protocol { case "trojan": clientId = oldClient.Password case "shadowsocks": clientId = oldClient.Email default: clientId = oldClient.ID } break } } if len(clientId) == 0 { return false, common.NewError("Client Not Found For Email:", clientEmail) } var settings map[string]any err = json.Unmarshal([]byte(inbound.Settings), &settings) if err != nil { return false, err } clients := settings["clients"].([]any) var newClients []any for client_index := range clients { c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["tgId"] = tgId c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } settings["clients"] = newClients modifiedSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, err } inbound.Settings = string(modifiedSettings) needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId) return needRestart, err } func (s *ClientService) checkIsEnabledByEmail(inboundSvc *InboundService, clientEmail string) (bool, error) { _, inbound, err := inboundSvc.GetClientInboundByEmail(clientEmail) if err != nil { return false, err } if inbound == nil { return false, common.NewError("Inbound Not Found For Email:", clientEmail) } clients, err := inboundSvc.GetClients(inbound) if err != nil { return false, err } isEnable := false for _, client := range clients { if client.Email == clientEmail { isEnable = client.Enable break } } return isEnable, err } func (s *ClientService) ToggleClientEnableByEmail(inboundSvc *InboundService, clientEmail string) (bool, bool, error) { _, inbound, err := inboundSvc.GetClientInboundByEmail(clientEmail) if err != nil { return false, false, err } if inbound == nil { return false, false, common.NewError("Inbound Not Found For Email:", clientEmail) } oldClients, err := inboundSvc.GetClients(inbound) if err != nil { return false, false, err } clientId := "" clientOldEnabled := false for _, oldClient := range oldClients { if oldClient.Email == clientEmail { switch inbound.Protocol { case "trojan": clientId = oldClient.Password case "shadowsocks": clientId = oldClient.Email default: clientId = oldClient.ID } clientOldEnabled = oldClient.Enable break } } if len(clientId) == 0 { return false, false, common.NewError("Client Not Found For Email:", clientEmail) } var settings map[string]any err = json.Unmarshal([]byte(inbound.Settings), &settings) if err != nil { return false, false, err } clients := settings["clients"].([]any) var newClients []any for client_index := range clients { c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["enable"] = !clientOldEnabled c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } settings["clients"] = newClients modifiedSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, false, err } inbound.Settings = string(modifiedSettings) needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId) if err != nil { return false, needRestart, err } return !clientOldEnabled, needRestart, nil } func (s *ClientService) SetClientEnableByEmail(inboundSvc *InboundService, clientEmail string, enable bool) (bool, bool, error) { current, err := s.checkIsEnabledByEmail(inboundSvc, clientEmail) if err != nil { return false, false, err } if current == enable { return false, false, nil } newEnabled, needRestart, err := s.ToggleClientEnableByEmail(inboundSvc, clientEmail) if err != nil { return false, needRestart, err } return newEnabled == enable, needRestart, nil } func (s *ClientService) ResetClientIpLimitByEmail(inboundSvc *InboundService, clientEmail string, count int) (bool, error) { _, inbound, err := inboundSvc.GetClientInboundByEmail(clientEmail) if err != nil { return false, err } if inbound == nil { return false, common.NewError("Inbound Not Found For Email:", clientEmail) } oldClients, err := inboundSvc.GetClients(inbound) if err != nil { return false, err } clientId := "" for _, oldClient := range oldClients { if oldClient.Email == clientEmail { switch inbound.Protocol { case "trojan": clientId = oldClient.Password case "shadowsocks": clientId = oldClient.Email default: clientId = oldClient.ID } break } } if len(clientId) == 0 { return false, common.NewError("Client Not Found For Email:", clientEmail) } var settings map[string]any err = json.Unmarshal([]byte(inbound.Settings), &settings) if err != nil { return false, err } clients := settings["clients"].([]any) var newClients []any for client_index := range clients { c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["limitIp"] = count c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } settings["clients"] = newClients modifiedSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, err } inbound.Settings = string(modifiedSettings) needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId) return needRestart, err } func (s *ClientService) ResetClientExpiryTimeByEmail(inboundSvc *InboundService, clientEmail string, expiry_time int64) (bool, error) { _, inbound, err := inboundSvc.GetClientInboundByEmail(clientEmail) if err != nil { return false, err } if inbound == nil { return false, common.NewError("Inbound Not Found For Email:", clientEmail) } oldClients, err := inboundSvc.GetClients(inbound) if err != nil { return false, err } clientId := "" for _, oldClient := range oldClients { if oldClient.Email == clientEmail { switch inbound.Protocol { case "trojan": clientId = oldClient.Password case "shadowsocks": clientId = oldClient.Email default: clientId = oldClient.ID } break } } if len(clientId) == 0 { return false, common.NewError("Client Not Found For Email:", clientEmail) } var settings map[string]any err = json.Unmarshal([]byte(inbound.Settings), &settings) if err != nil { return false, err } clients := settings["clients"].([]any) var newClients []any for client_index := range clients { c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["expiryTime"] = expiry_time c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } settings["clients"] = newClients modifiedSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, err } inbound.Settings = string(modifiedSettings) needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId) return needRestart, err } func (s *ClientService) ResetClientTrafficLimitByEmail(inboundSvc *InboundService, clientEmail string, totalGB int) (bool, error) { if totalGB < 0 { return false, common.NewError("totalGB must be >= 0") } _, inbound, err := inboundSvc.GetClientInboundByEmail(clientEmail) if err != nil { return false, err } if inbound == nil { return false, common.NewError("Inbound Not Found For Email:", clientEmail) } oldClients, err := inboundSvc.GetClients(inbound) if err != nil { return false, err } clientId := "" for _, oldClient := range oldClients { if oldClient.Email == clientEmail { switch inbound.Protocol { case "trojan": clientId = oldClient.Password case "shadowsocks": clientId = oldClient.Email default: clientId = oldClient.ID } break } } if len(clientId) == 0 { return false, common.NewError("Client Not Found For Email:", clientEmail) } var settings map[string]any err = json.Unmarshal([]byte(inbound.Settings), &settings) if err != nil { return false, err } clients := settings["clients"].([]any) var newClients []any for client_index := range clients { c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["totalGB"] = totalGB * 1024 * 1024 * 1024 c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } settings["clients"] = newClients modifiedSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, err } inbound.Settings = string(modifiedSettings) needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId) return needRestart, err }