Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMHSanaei <ho3ein.sanaei@gmail.com>2026-05-05 00:19:25 +0300
committerMHSanaei <ho3ein.sanaei@gmail.com>2026-05-05 00:19:25 +0300
commit6099a07ff0a3f619ace3c7645ee76dce943a97e0 (patch)
tree10919163dd9a978908ed5fb16baa6a3a81cf32bf /web/service/inbound.go
parente9806832ec222e87052c3b8263f392bf5a41974e (diff)
feat: add configurable auto-restart on client auto-disable
Add a configurable option to restart Xray when clients are auto-disabled and persist disable actions. Changes include: - New setting restartXrayOnClientDisable (default true), getters/setters in SettingService, UI toggle in general settings, and translations for multiple locales. - AddTraffic signature updated to return a third bool (clientsDisabled). disableInvalidClients now calls Xray API to remove users, marks client_traffics.enable=false, updates inbound.Settings JSON so clients appear disabled in stored settings, and returns appropriate counts/errors. - XrayTrafficJob now checks the clientsDisabled flag and restarts Xray when the setting is enabled (with fallback to mark Xray as needing restart on failure). - XrayService.GetXrayConfig call adjusted to ignore AddTraffic returns. - Subscription generation (subService/subJson/subClash) no longer filters clients by their enable flag when matching subId. - Minor fixes: check_client_ip_job now checks scanner.Err and improved API error handling/logging. These changes ensure auto-disabled clients are propagated to Xray and the stored inbound settings, and provide an option to restart Xray automatically after auto-disable events.
Diffstat (limited to 'web/service/inbound.go')
-rw-r--r--web/service/inbound.go119
1 files changed, 90 insertions, 29 deletions
diff --git a/web/service/inbound.go b/web/service/inbound.go
index 74b44b99..f536efb9 100644
--- a/web/service/inbound.go
+++ b/web/service/inbound.go
@@ -1228,7 +1228,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
return needRestart, tx.Save(oldInbound).Error
}
-func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
+func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool, bool) {
var err error
db := database.GetDB()
tx := db.Begin()
@@ -1242,11 +1242,11 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
}()
err = s.addInboundTraffic(tx, inboundTraffics)
if err != nil {
- return err, false
+ return err, false, false
}
err = s.addClientTraffic(tx, clientTraffics)
if err != nil {
- return err, false
+ return err, false, false
}
needRestart0, count, err := s.autoRenewClients(tx)
@@ -1256,11 +1256,13 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
logger.Debugf("%v clients renewed", count)
}
+ disabledClientsCount := int64(0)
needRestart1, count, err := s.disableInvalidClients(tx)
if err != nil {
logger.Warning("Error in disabling invalid clients:", err)
} else if count > 0 {
logger.Debugf("%v clients disabled", count)
+ disabledClientsCount = count
}
needRestart2, count, err := s.disableInvalidInbounds(tx)
@@ -1269,7 +1271,7 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
} else if count > 0 {
logger.Debugf("%v inbounds disabled", count)
}
- return nil, (needRestart0 || needRestart1 || needRestart2)
+ return nil, (needRestart0 || needRestart1 || needRestart2), disabledClientsCount > 0
}
func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
@@ -1546,46 +1548,105 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
now := time.Now().Unix() * 1000
needRestart := false
- if p != nil {
- var results []struct {
- Tag string
- Email string
- }
+ var clientsToDisable []struct {
+ InboundId int
+ Tag string
+ Email string
+ }
- err := tx.Table("inbounds").
- Select("inbounds.tag, client_traffics.email").
- Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id").
- Where("((client_traffics.total > 0 AND client_traffics.up + client_traffics.down >= client_traffics.total) OR (client_traffics.expiry_time > 0 AND client_traffics.expiry_time <= ?)) AND client_traffics.enable = ?", now, true).
- Scan(&results).Error
- if err != nil {
- return false, 0, err
- }
+ err := tx.Table("inbounds").
+ Select("inbounds.id as inbound_id, inbounds.tag, client_traffics.email").
+ Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id").
+ Where("((client_traffics.total > 0 AND client_traffics.up + client_traffics.down >= client_traffics.total) OR (client_traffics.expiry_time > 0 AND client_traffics.expiry_time <= ?)) AND client_traffics.enable = ?", now, true).
+ Scan(&clientsToDisable).Error
+ if err != nil {
+ return false, 0, err
+ }
+
+ if p != nil {
s.xrayApi.Init(p.GetAPIPort())
- for _, result := range results {
- err1 := s.xrayApi.RemoveUser(result.Tag, result.Email)
+ for _, client := range clientsToDisable {
+ err1 := s.xrayApi.RemoveUser(client.Tag, client.Email)
if err1 == nil {
- logger.Debug("Client disabled by api:", result.Email)
+ logger.Debug("Client disabled by api:", client.Email)
} else {
- if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", result.Email)) {
+ if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", client.Email)) {
logger.Debug("User is already disabled. Nothing to do more...")
} else {
- if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", result.Email)) {
- logger.Debug("User is already disabled. Nothing to do more...")
- } else {
- logger.Debug("Error in disabling client by api:", err1)
- needRestart = true
- }
+ logger.Debug("Error in disabling client by api:", err1)
+ needRestart = true
}
}
}
s.xrayApi.Close()
}
+
result := tx.Model(xray.ClientTraffic{}).
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
Update("enable", false)
- err := result.Error
+ err = result.Error
count := result.RowsAffected
- return needRestart, count, err
+ if err != nil {
+ return needRestart, count, err
+ }
+
+ // Also set enable=false in inbounds.settings JSON so clients are visibly disabled
+ if len(clientsToDisable) > 0 {
+ inboundEmailMap := make(map[int]map[string]struct{})
+ for _, c := range clientsToDisable {
+ if inboundEmailMap[c.InboundId] == nil {
+ inboundEmailMap[c.InboundId] = make(map[string]struct{})
+ }
+ inboundEmailMap[c.InboundId][c.Email] = struct{}{}
+ }
+ inboundIds := make([]int, 0, len(inboundEmailMap))
+ for id := range inboundEmailMap {
+ inboundIds = append(inboundIds, id)
+ }
+ var inbounds []*model.Inbound
+ if err = tx.Model(model.Inbound{}).Where("id IN ?", inboundIds).Find(&inbounds).Error; err != nil {
+ logger.Warning("disableInvalidClients fetch inbounds:", err)
+ return needRestart, count, nil
+ }
+ for _, inbound := range inbounds {
+ settings := map[string]any{}
+ if jsonErr := json.Unmarshal([]byte(inbound.Settings), &settings); jsonErr != nil {
+ continue
+ }
+ clients, ok := settings["clients"].([]any)
+ if !ok {
+ continue
+ }
+ emailSet := inboundEmailMap[inbound.Id]
+ changed := false
+ for i := range clients {
+ c, ok := clients[i].(map[string]any)
+ if !ok {
+ continue
+ }
+ email, _ := c["email"].(string)
+ if _, shouldDisable := emailSet[email]; shouldDisable {
+ c["enable"] = false
+ c["updated_at"] = time.Now().Unix() * 1000
+ clients[i] = c
+ changed = true
+ }
+ }
+ if changed {
+ settings["clients"] = clients
+ modifiedSettings, jsonErr := json.MarshalIndent(settings, "", " ")
+ if jsonErr != nil {
+ continue
+ }
+ inbound.Settings = string(modifiedSettings)
+ }
+ }
+ if err = tx.Save(inbounds).Error; err != nil {
+ logger.Warning("disableInvalidClients update inbound settings:", err)
+ }
+ }
+
+ return needRestart, count, nil
}
func (s *InboundService) GetInboundTags() (string, error) {