mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-31 07:27:13 +00:00
fix(traffic): apply maintenance side effects only after the commit lands (#6200)
* fix(traffic): apply maintenance after durable commit * fix(traffic): apply all runtime maintenance after commit --------- Co-authored-by: n0ctal <293235942+n0ctal@users.noreply.github.com>
This commit is contained in:
@@ -4,6 +4,7 @@ export type ProcessState = string;
|
|||||||
export type Protocol = string;
|
export type Protocol = string;
|
||||||
export type SubLinkProvider = unknown;
|
export type SubLinkProvider = unknown;
|
||||||
export type staticEgressResolver = string;
|
export type staticEgressResolver = string;
|
||||||
|
export type trafficLocalApplyAction = number;
|
||||||
export type transportBits = number;
|
export type transportBits = number;
|
||||||
|
|
||||||
export interface AllSetting {
|
export interface AllSetting {
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export type SubLinkProvider = z.infer<typeof SubLinkProviderSchema>;
|
|||||||
export const staticEgressResolverSchema = z.string();
|
export const staticEgressResolverSchema = z.string();
|
||||||
export type staticEgressResolver = z.infer<typeof staticEgressResolverSchema>;
|
export type staticEgressResolver = z.infer<typeof staticEgressResolverSchema>;
|
||||||
|
|
||||||
|
export const trafficLocalApplyActionSchema = z.number().int();
|
||||||
|
export type trafficLocalApplyAction = z.infer<typeof trafficLocalApplyActionSchema>;
|
||||||
|
|
||||||
export const transportBitsSchema = z.number().int();
|
export const transportBitsSchema = z.number().int();
|
||||||
export type transportBits = z.infer<typeof transportBitsSchema>;
|
export type transportBits = z.infer<typeof transportBitsSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func TestDepletedCond_ProbeGuard(t *testing.T) {
|
|||||||
t.Fatalf("empty globals must use the local-only predicate")
|
t.Fatalf("empty globals must use the local-only predicate")
|
||||||
}
|
}
|
||||||
seedClientRow(t, "local-cap", 1, 600, 600, 1000)
|
seedClientRow(t, "local-cap", 1, 600, 600, 1000)
|
||||||
if _, count, err := svc.disableInvalidClients(db); err != nil {
|
if _, count, _, err := svc.disableInvalidClients(db, newTrafficMutationBatch()); err != nil {
|
||||||
t.Fatalf("disableInvalidClients: %v", err)
|
t.Fatalf("disableInvalidClients: %v", err)
|
||||||
} else if count != 1 {
|
} else if count != 1 {
|
||||||
t.Fatalf("local over-quota client must be disabled, disabled %d", count)
|
t.Fatalf("local over-quota client must be disabled, disabled %d", count)
|
||||||
@@ -115,7 +115,7 @@ func TestStaleGlobalTraffic_Ignored(t *testing.T) {
|
|||||||
if got, _ := depletedCond(db); got != depletedClientsCondLocal {
|
if got, _ := depletedCond(db); got != depletedClientsCondLocal {
|
||||||
t.Fatalf("only stale globals must fall back to the local-only predicate")
|
t.Fatalf("only stale globals must fall back to the local-only predicate")
|
||||||
}
|
}
|
||||||
if _, count, err := svc.disableInvalidClients(db); err != nil {
|
if _, count, _, err := svc.disableInvalidClients(db, newTrafficMutationBatch()); err != nil {
|
||||||
t.Fatalf("disableInvalidClients: %v", err)
|
t.Fatalf("disableInvalidClients: %v", err)
|
||||||
} else if count != 0 {
|
} else if count != 0 {
|
||||||
t.Fatalf("stale global usage must not disable a client, disabled %d", count)
|
t.Fatalf("stale global usage must not disable a client, disabled %d", count)
|
||||||
@@ -140,7 +140,7 @@ func TestStaleGlobalTraffic_Ignored(t *testing.T) {
|
|||||||
if got, _ := depletedCond(db); got != depletedClientsCond {
|
if got, _ := depletedCond(db); got != depletedClientsCond {
|
||||||
t.Fatalf("a fresh global row must select the cross-panel predicate")
|
t.Fatalf("a fresh global row must select the cross-panel predicate")
|
||||||
}
|
}
|
||||||
if _, count, err := svc.disableInvalidClients(db); err != nil {
|
if _, count, _, err := svc.disableInvalidClients(db, newTrafficMutationBatch()); err != nil {
|
||||||
t.Fatalf("disableInvalidClients: %v", err)
|
t.Fatalf("disableInvalidClients: %v", err)
|
||||||
} else if count != 0 {
|
} else if count != 0 {
|
||||||
t.Fatalf("the live master reports usage well under quota, disabled %d", count)
|
t.Fatalf("the live master reports usage well under quota, disabled %d", count)
|
||||||
@@ -149,7 +149,7 @@ func TestStaleGlobalTraffic_Ignored(t *testing.T) {
|
|||||||
if err := svc.AcceptGlobalTraffic("live-master", []*xray.ClientTraffic{{Email: "cap", Up: 600, Down: 500}}); err != nil {
|
if err := svc.AcceptGlobalTraffic("live-master", []*xray.ClientTraffic{{Email: "cap", Up: 600, Down: 500}}); err != nil {
|
||||||
t.Fatalf("AcceptGlobalTraffic: %v", err)
|
t.Fatalf("AcceptGlobalTraffic: %v", err)
|
||||||
}
|
}
|
||||||
if _, count, err := svc.disableInvalidClients(db); err != nil {
|
if _, count, _, err := svc.disableInvalidClients(db, newTrafficMutationBatch()); err != nil {
|
||||||
t.Fatalf("disableInvalidClients: %v", err)
|
t.Fatalf("disableInvalidClients: %v", err)
|
||||||
} else if count != 1 {
|
} else if count != 1 {
|
||||||
t.Fatalf("fresh cross-panel depletion must disable the client, disabled %d", count)
|
t.Fatalf("fresh cross-panel depletion must disable the client, disabled %d", count)
|
||||||
@@ -167,7 +167,7 @@ func TestGlobalUsage_DisablesClient(t *testing.T) {
|
|||||||
t.Fatalf("AcceptGlobalTraffic: %v", err)
|
t.Fatalf("AcceptGlobalTraffic: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, count, err := svc.disableInvalidClients(db); err != nil {
|
if _, count, _, err := svc.disableInvalidClients(db, newTrafficMutationBatch()); err != nil {
|
||||||
t.Fatalf("disableInvalidClients: %v", err)
|
t.Fatalf("disableInvalidClients: %v", err)
|
||||||
} else if count != 1 {
|
} else if count != 1 {
|
||||||
t.Fatalf("expected 1 client disabled, got %d", count)
|
t.Fatalf("expected 1 client disabled, got %d", count)
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type InboundService struct {
|
type InboundService struct {
|
||||||
xrayApi xray.XrayAPI
|
|
||||||
clientService ClientService
|
clientService ClientService
|
||||||
fallbackService FallbackService
|
fallbackService FallbackService
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func TestAutoRenewShadowsocksKeepsSettingsClean(t *testing.T) {
|
|||||||
t.Fatalf("seed client_traffics: %v", err)
|
t.Fatalf("seed client_traffics: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, count, err := svc.autoRenewClients(db); err != nil {
|
if _, count, err := svc.autoRenewClients(db, newTrafficMutationBatch()); err != nil {
|
||||||
t.Fatalf("autoRenewClients: %v", err)
|
t.Fatalf("autoRenewClients: %v", err)
|
||||||
} else if count != 1 {
|
} else if count != 1 {
|
||||||
t.Fatalf("renewed count = %d, want 1", count)
|
t.Fatalf("renewed count = %d, want 1", count)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func TestAutoRenewClients_MultiInbound(t *testing.T) {
|
|||||||
t.Fatalf("seed client_traffics: %v", err)
|
t.Fatalf("seed client_traffics: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, count, err := svc.autoRenewClients(db); err != nil {
|
if _, count, err := svc.autoRenewClients(db, newTrafficMutationBatch()); err != nil {
|
||||||
t.Fatalf("autoRenewClients: %v", err)
|
t.Fatalf("autoRenewClients: %v", err)
|
||||||
} else if count != 3 {
|
} else if count != 3 {
|
||||||
t.Fatalf("renewed count = %d, want 3", count)
|
t.Fatalf("renewed count = %d, want 3", count)
|
||||||
|
|||||||
@@ -1,44 +1,27 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *InboundService) disableInvalidInbounds(tx *gorm.DB) (bool, int64, error) {
|
func (s *InboundService) disableInvalidInbounds(tx *gorm.DB, mutationBatch *trafficMutationBatch) (bool, int64, error) {
|
||||||
now := time.Now().Unix() * 1000
|
now := time.Now().Unix() * 1000
|
||||||
needRestart := false
|
var inbounds []model.Inbound
|
||||||
|
if err := tx.Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ? and node_id IS NULL", now, true).
|
||||||
if process := currentXrayProcess(); process != nil {
|
Find(&inbounds).Error; err != nil {
|
||||||
var tags []string
|
return false, 0, err
|
||||||
err := tx.Table("inbounds").
|
}
|
||||||
Select("inbounds.tag").
|
for i := range inbounds {
|
||||||
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ? and node_id IS NULL", now, true).
|
mutationBatch.localPlans = append(mutationBatch.localPlans, trafficLocalApplyPlan{
|
||||||
Scan(&tags).Error
|
action: trafficDisableInbound, inbound: inbounds[i],
|
||||||
if err != nil {
|
})
|
||||||
return false, 0, err
|
|
||||||
}
|
|
||||||
_ = s.xrayApi.Init(process.GetAPIPort())
|
|
||||||
for _, tag := range tags {
|
|
||||||
err1 := s.xrayApi.DelInbound(tag)
|
|
||||||
if err1 == nil {
|
|
||||||
logger.Debug("Inbound disabled by api:", tag)
|
|
||||||
} else {
|
|
||||||
logger.Debug("Error in disabling inbound by api:", err1)
|
|
||||||
needRestart = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.xrayApi.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result := tx.Model(model.Inbound{}).
|
result := tx.Model(model.Inbound{}).
|
||||||
@@ -46,7 +29,7 @@ func (s *InboundService) disableInvalidInbounds(tx *gorm.DB) (bool, int64, error
|
|||||||
Update("enable", false)
|
Update("enable", false)
|
||||||
err := result.Error
|
err := result.Error
|
||||||
count := result.RowsAffected
|
count := result.RowsAffected
|
||||||
return needRestart, count, err
|
return false, count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalTrafficFreshWindow = 24 * time.Hour
|
const globalTrafficFreshWindow = 24 * time.Hour
|
||||||
@@ -94,8 +77,8 @@ func depletedCond(tx *gorm.DB) (string, []any) {
|
|||||||
return depletedClientsCondLocal, []any{now}
|
return depletedClientsCondLocal, []any{now}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error) {
|
func (s *InboundService) disableInvalidClients(tx *gorm.DB, mutationBatch *trafficMutationBatch) (bool, int64, []int, error) {
|
||||||
needRestart := false
|
now := time.Now().UnixMilli()
|
||||||
cond, condArgs := depletedCond(tx)
|
cond, condArgs := depletedCond(tx)
|
||||||
|
|
||||||
var depletedRows []xray.ClientTraffic
|
var depletedRows []xray.ClientTraffic
|
||||||
@@ -103,10 +86,10 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
|
|||||||
Where(cond+" AND enable = ?", append(condArgs, true)...).
|
Where(cond+" AND enable = ?", append(condArgs, true)...).
|
||||||
Find(&depletedRows).Error
|
Find(&depletedRows).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, 0, err
|
return false, 0, nil, err
|
||||||
}
|
}
|
||||||
if len(depletedRows) == 0 {
|
if len(depletedRows) == 0 {
|
||||||
return false, 0, nil
|
return false, 0, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
depletedEmails := make([]string, 0, len(depletedRows))
|
depletedEmails := make([]string, 0, len(depletedRows))
|
||||||
@@ -134,47 +117,39 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
|
|||||||
WHERE clients.email IN ?
|
WHERE clients.email IN ?
|
||||||
`, depletedEmails).Scan(&targets).Error
|
`, depletedEmails).Scan(&targets).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, 0, err
|
return false, 0, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var localTargets []target
|
byInbound := make(map[int][]target)
|
||||||
localByInbound := make(map[int]map[string]struct{})
|
|
||||||
remoteByInbound := make(map[int][]target)
|
|
||||||
for _, t := range targets {
|
for _, t := range targets {
|
||||||
if t.NodeID == nil {
|
byInbound[t.InboundID] = append(byInbound[t.InboundID], t)
|
||||||
localTargets = append(localTargets, t)
|
|
||||||
if localByInbound[t.InboundID] == nil {
|
|
||||||
localByInbound[t.InboundID] = make(map[string]struct{})
|
|
||||||
}
|
|
||||||
localByInbound[t.InboundID][t.Email] = struct{}{}
|
|
||||||
} else {
|
|
||||||
remoteByInbound[t.InboundID] = append(remoteByInbound[t.InboundID], t)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if process := currentXrayProcess(); process != nil && len(localTargets) > 0 {
|
disabledNodeIDs := make(map[int]struct{})
|
||||||
_ = s.xrayApi.Init(process.GetAPIPort())
|
for inboundID, group := range byInbound {
|
||||||
for _, t := range localTargets {
|
emails := make(map[string]struct{}, len(group))
|
||||||
err1 := s.xrayApi.RemoveUser(t.Tag, t.Email)
|
for _, t := range group {
|
||||||
if err1 == nil {
|
emails[t.Email] = struct{}{}
|
||||||
logger.Debug("Client disabled by api:", t.Email)
|
|
||||||
} else if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", t.Email)) {
|
|
||||||
logger.Debug("User is already disabled. Nothing to do more...")
|
|
||||||
} else {
|
|
||||||
logger.Debug("Error in disabling client by api:", err1)
|
|
||||||
needRestart = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
s.xrayApi.Close()
|
oldInbound, inbound, mErr := s.markClientsDisabledInSettings(tx, inboundID, emails)
|
||||||
}
|
if mErr != nil {
|
||||||
|
return false, 0, nil, mErr
|
||||||
for inboundID, emails := range localByInbound {
|
}
|
||||||
if _, _, mErr := s.markClientsDisabledInSettings(tx, inboundID, emails); mErr != nil {
|
if inbound.NodeID != nil {
|
||||||
logger.Warning("disableInvalidClients: settings.JSON sync failed for inbound", inboundID, ":", mErr)
|
mutationBatch.remotePlans = append(mutationBatch.remotePlans, trafficInboundUpdatePlan{
|
||||||
|
oldInbound: *oldInbound, newInbound: *inbound,
|
||||||
|
})
|
||||||
|
mutationBatch.addNode(*inbound.NodeID)
|
||||||
|
disabledNodeIDs[*inbound.NodeID] = struct{}{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for email := range emails {
|
||||||
|
mutationBatch.localPlans = append(mutationBatch.localPlans, trafficLocalApplyPlan{
|
||||||
|
action: trafficRemoveUser, inbound: *inbound, email: email,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flip the rows already collected above by primary key instead of
|
// Flip the rows already collected above by primary key instead of
|
||||||
// re-evaluating the depleted predicate, which was a second full scan of
|
// re-evaluating the depleted predicate, which was a second full scan of
|
||||||
// client_traffics on every poll. Sorted ids keep the lock order stable.
|
// client_traffics on every poll. Sorted ids keep the lock order stable.
|
||||||
@@ -189,7 +164,7 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
|
|||||||
Where("id IN ? AND enable = ?", batch, true).
|
Where("id IN ? AND enable = ?", batch, true).
|
||||||
Update("enable", false)
|
Update("enable", false)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return needRestart, count, result.Error
|
return false, count, nil, result.Error
|
||||||
}
|
}
|
||||||
count += result.RowsAffected
|
count += result.RowsAffected
|
||||||
}
|
}
|
||||||
@@ -197,23 +172,17 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
|
|||||||
if len(depletedEmails) > 0 {
|
if len(depletedEmails) > 0 {
|
||||||
if err := tx.Model(&model.ClientRecord{}).
|
if err := tx.Model(&model.ClientRecord{}).
|
||||||
Where("email IN ?", depletedEmails).
|
Where("email IN ?", depletedEmails).
|
||||||
Updates(map[string]any{"enable": false, "updated_at": time.Now().UnixMilli()}).Error; err != nil {
|
Updates(map[string]any{"enable": false, "updated_at": now}).Error; err != nil {
|
||||||
logger.Warning("disableInvalidClients update clients.enable:", err)
|
return false, count, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for inboundID, group := range remoteByInbound {
|
nodeIDs := make([]int, 0, len(disabledNodeIDs))
|
||||||
emails := make(map[string]struct{}, len(group))
|
for nodeID := range disabledNodeIDs {
|
||||||
for _, t := range group {
|
nodeIDs = append(nodeIDs, nodeID)
|
||||||
emails[t.Email] = struct{}{}
|
|
||||||
}
|
|
||||||
if pushErr := s.disableRemoteClients(tx, inboundID, emails); pushErr != nil {
|
|
||||||
logger.Warning("disableInvalidClients: push to remote failed for inbound", inboundID, ":", pushErr)
|
|
||||||
needRestart = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return needRestart, count, nil
|
return false, count, nodeIDs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// markClientsDisabledInSettings flips client.enable=false in the inbound's
|
// markClientsDisabledInSettings flips client.enable=false in the inbound's
|
||||||
@@ -265,23 +234,3 @@ func (s *InboundService) markClientsDisabledInSettings(tx *gorm.DB, inboundID in
|
|||||||
}
|
}
|
||||||
return &snapshot, &ib, nil
|
return &snapshot, &ib, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// disableRemoteClients flips the clients off in the inbound's stored settings
|
|
||||||
// and pushes the updated inbound to its node, which applies it to its own
|
|
||||||
// running Xray. That push is the whole reconcile — restarting the node's Xray
|
|
||||||
// afterwards would drop every live connection on the node for nothing (#5740).
|
|
||||||
func (s *InboundService) disableRemoteClients(tx *gorm.DB, inboundID int, emails map[string]struct{}) error {
|
|
||||||
oldSnapshot, ib, err := s.markClientsDisabledInSettings(tx, inboundID, emails)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rt, err := s.runtimeFor(ib)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := rt.UpdateInbound(context.Background(), oldSnapshot, ib); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1079,6 +1079,28 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
|||||||
return structuralChange, nil
|
return structuralChange, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) restartRemoteNodesOnDisable(nodeIDs []int) {
|
||||||
|
restartOnDisable, err := (&SettingService{}).GetRestartXrayOnClientDisable()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("disableInvalidClients: get RestartXrayOnClientDisable failed:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !restartOnDisable {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, nodeID := range nodeIDs {
|
||||||
|
nodeIDCopy := nodeID
|
||||||
|
rt, rtErr := runtime.GetManager().RuntimeFor(&nodeIDCopy)
|
||||||
|
if rtErr != nil {
|
||||||
|
logger.Warning("disableInvalidClients: get runtime for node", nodeID, "failed:", rtErr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rtErr = rt.RestartXray(context.Background()); rtErr != nil {
|
||||||
|
logger.Warning("disableInvalidClients: restart xray on node", nodeID, "failed:", rtErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *InboundService) GetOnlineClients() []string {
|
func (s *InboundService) GetOnlineClients() []string {
|
||||||
process := currentXrayProcess()
|
process := currentXrayProcess()
|
||||||
if process == nil {
|
if process == nil {
|
||||||
|
|||||||
@@ -22,60 +22,77 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (needRestart bool, clientsDisabled bool, err error) {
|
func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (needRestart bool, clientsDisabled bool, err error) {
|
||||||
|
var disabledNodeIDs []int
|
||||||
err = submitTrafficWrite(func() error {
|
err = submitTrafficWrite(func() error {
|
||||||
var inner error
|
var inner error
|
||||||
needRestart, clientsDisabled, inner = s.addTrafficLocked(inboundTraffics, clientTraffics)
|
needRestart, clientsDisabled, disabledNodeIDs, inner = s.addTrafficLocked(inboundTraffics, clientTraffics)
|
||||||
return inner
|
return inner
|
||||||
})
|
})
|
||||||
|
if err == nil && len(disabledNodeIDs) > 0 {
|
||||||
|
s.restartRemoteNodesOnDisable(disabledNodeIDs)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) addTrafficLocked(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (bool, bool, error) {
|
func (s *InboundService) addTrafficLocked(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (bool, bool, []int, error) {
|
||||||
var err error
|
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
tx := db.Begin()
|
// Commit durable traffic before best-effort lifecycle maintenance so helper
|
||||||
|
// failures cannot discard usage already reported by Xray.
|
||||||
defer func() {
|
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
if err != nil {
|
if err := s.addInboundTraffic(tx, inboundTraffics); err != nil {
|
||||||
if rbErr := tx.Rollback().Error; rbErr != nil {
|
return err
|
||||||
logger.Warning("Error rolling back traffic tx:", rbErr)
|
|
||||||
}
|
|
||||||
} else if cErr := tx.Commit().Error; cErr != nil {
|
|
||||||
logger.Warning("Error committing traffic tx:", cErr)
|
|
||||||
}
|
}
|
||||||
}()
|
return s.addClientTraffic(tx, clientTraffics)
|
||||||
err = s.addInboundTraffic(tx, inboundTraffics)
|
}); err != nil {
|
||||||
|
return false, false, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
needRestart bool
|
||||||
|
clientsDisabled bool
|
||||||
|
disabledNodeIDs []int
|
||||||
|
disabledClientsCount int64
|
||||||
|
)
|
||||||
|
batch := newTrafficMutationBatch()
|
||||||
|
err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
needRestart0, count, err := s.autoRenewClients(tx, batch)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("renew clients: %w", err)
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
logger.Debugf("%v clients renewed", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
needRestart1, count, nodeIDs, err := s.disableInvalidClients(tx, batch)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("disable invalid clients: %w", err)
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
logger.Debugf("%v clients disabled", count)
|
||||||
|
disabledClientsCount = count
|
||||||
|
}
|
||||||
|
|
||||||
|
needRestart2, count, err := s.disableInvalidInbounds(tx, batch)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("disable invalid inbounds: %w", err)
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
logger.Debugf("%v inbounds disabled", count)
|
||||||
|
}
|
||||||
|
if err := batch.markNodesTx(tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
needRestart = needRestart0 || needRestart1 || needRestart2
|
||||||
|
clientsDisabled = disabledClientsCount > 0
|
||||||
|
disabledNodeIDs = nodeIDs
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, err
|
logger.Warning("traffic lifecycle maintenance failed after traffic commit:", err)
|
||||||
|
return false, false, nil, nil
|
||||||
}
|
}
|
||||||
err = s.addClientTraffic(tx, clientTraffics)
|
needRestart = needRestart || s.applyTrafficMutationBatch(batch)
|
||||||
if err != nil {
|
return needRestart, clientsDisabled, disabledNodeIDs, nil
|
||||||
return false, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
needRestart0, count, renewErr := s.autoRenewClients(tx)
|
|
||||||
if renewErr != nil {
|
|
||||||
logger.Warning("Error in renew clients:", renewErr)
|
|
||||||
} else if count > 0 {
|
|
||||||
logger.Debugf("%v clients renewed", count)
|
|
||||||
}
|
|
||||||
|
|
||||||
disabledClientsCount := int64(0)
|
|
||||||
needRestart1, count, disableClientsErr := s.disableInvalidClients(tx)
|
|
||||||
if disableClientsErr != nil {
|
|
||||||
logger.Warning("Error in disabling invalid clients:", disableClientsErr)
|
|
||||||
} else if count > 0 {
|
|
||||||
logger.Debugf("%v clients disabled", count)
|
|
||||||
disabledClientsCount = count
|
|
||||||
}
|
|
||||||
|
|
||||||
needRestart2, count, disableInboundsErr := s.disableInvalidInbounds(tx)
|
|
||||||
if disableInboundsErr != nil {
|
|
||||||
logger.Warning("Error in disabling invalid inbounds:", disableInboundsErr)
|
|
||||||
} else if count > 0 {
|
|
||||||
logger.Debugf("%v inbounds disabled", count)
|
|
||||||
}
|
|
||||||
return needRestart0 || needRestart1 || needRestart2, disabledClientsCount > 0, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
|
func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
|
||||||
@@ -304,11 +321,11 @@ func apiUserFromClient(client map[string]any, cipher string) map[string]any {
|
|||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
|
func (s *InboundService) autoRenewClients(tx *gorm.DB, mutationBatch *trafficMutationBatch) (bool, int64, error) {
|
||||||
// check for time expired
|
// check for time expired
|
||||||
var traffics []*xray.ClientTraffic
|
var traffics []*xray.ClientTraffic
|
||||||
now := time.Now().Unix() * 1000
|
now := time.Now().Unix() * 1000
|
||||||
var err, err1 error
|
var err error
|
||||||
|
|
||||||
// Filter to clients that have at least one local inbound. Using
|
// Filter to clients that have at least one local inbound. Using
|
||||||
// client_traffics.inbound_id is wrong: it goes stale after an inbound is
|
// client_traffics.inbound_id is wrong: it goes stale after an inbound is
|
||||||
@@ -335,9 +352,8 @@ func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
|
|||||||
var inbounds []*model.Inbound
|
var inbounds []*model.Inbound
|
||||||
needRestart := false
|
needRestart := false
|
||||||
var clientsToAdd []struct {
|
var clientsToAdd []struct {
|
||||||
protocol string
|
inbound model.Inbound
|
||||||
tag string
|
client map[string]any
|
||||||
client map[string]any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the inbounds to renew through the client_inbounds link rather than
|
// Resolve the inbounds to renew through the client_inbounds link rather than
|
||||||
@@ -408,13 +424,11 @@ func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
|
|||||||
c["enable"] = true
|
c["enable"] = true
|
||||||
clientsToAdd = append(clientsToAdd,
|
clientsToAdd = append(clientsToAdd,
|
||||||
struct {
|
struct {
|
||||||
protocol string
|
inbound model.Inbound
|
||||||
tag string
|
client map[string]any
|
||||||
client map[string]any
|
|
||||||
}{
|
}{
|
||||||
protocol: string(inbounds[inbound_index].Protocol),
|
inbound: *inbounds[inbound_index],
|
||||||
tag: inbounds[inbound_index].Tag,
|
client: apiUserFromClient(c, cipher),
|
||||||
client: apiUserFromClient(c, cipher),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
clients[client_index] = any(c)
|
clients[client_index] = any(c)
|
||||||
@@ -452,18 +466,14 @@ func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
|
|||||||
if err = clearGlobalTraffic(tx, renewEmails...); err != nil {
|
if err = clearGlobalTraffic(tx, renewEmails...); err != nil {
|
||||||
return false, 0, err
|
return false, 0, err
|
||||||
}
|
}
|
||||||
if process := currentXrayProcess(); process != nil {
|
for _, clientToAdd := range clientsToAdd {
|
||||||
err1 = s.xrayApi.Init(process.GetAPIPort())
|
if clientToAdd.inbound.NodeID != nil {
|
||||||
if err1 != nil {
|
mutationBatch.addNode(*clientToAdd.inbound.NodeID)
|
||||||
return true, int64(len(traffics)), nil
|
continue
|
||||||
}
|
}
|
||||||
for _, clientToAdd := range clientsToAdd {
|
mutationBatch.localPlans = append(mutationBatch.localPlans, trafficLocalApplyPlan{
|
||||||
err1 = s.xrayApi.AddUser(clientToAdd.protocol, clientToAdd.tag, clientToAdd.client)
|
action: trafficAddUser, inbound: clientToAdd.inbound, client: clientToAdd.client,
|
||||||
if err1 != nil {
|
})
|
||||||
needRestart = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.xrayApi.Close()
|
|
||||||
}
|
}
|
||||||
return needRestart, int64(len(traffics)), nil
|
return needRestart, int64(len(traffics)), nil
|
||||||
}
|
}
|
||||||
@@ -577,56 +587,58 @@ func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (needRestart bool, err error) {
|
func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (needRestart bool, err error) {
|
||||||
|
var resetInbound *model.Inbound
|
||||||
err = submitTrafficWrite(func() error {
|
err = submitTrafficWrite(func() error {
|
||||||
var inner error
|
var inner error
|
||||||
needRestart, inner = s.resetClientTrafficLocked(id, clientEmail)
|
needRestart, resetInbound, inner = s.resetClientTrafficLocked(id, clientEmail)
|
||||||
return inner
|
return inner
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
s.resetMtprotoClientQuota(clientEmail)
|
s.resetMtprotoClientQuota(clientEmail)
|
||||||
|
if resetInbound != nil && resetInbound.NodeID != nil {
|
||||||
|
if rt, rterr := s.runtimeFor(resetInbound); rterr == nil {
|
||||||
|
if e := rt.ResetClientTraffic(context.Background(), resetInbound, clientEmail); e != nil {
|
||||||
|
logger.Warning("ResetClientTraffic: remote propagation to", rt.Name(), "failed:", e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Warning("ResetClientTraffic: runtime lookup failed:", rterr)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) resetClientTrafficLocked(id int, clientEmail string) (bool, error) {
|
func (s *InboundService) resetClientTrafficLocked(id int, clientEmail string) (bool, *model.Inbound, error) {
|
||||||
needRestart := false
|
needRestart := false
|
||||||
|
var reenablePlan *trafficLocalApplyPlan
|
||||||
|
var reenableNodeID *int
|
||||||
|
|
||||||
traffic, err := s.GetClientTrafficByEmail(clientEmail)
|
traffic, err := s.GetClientTrafficByEmail(clientEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !traffic.Enable {
|
if !traffic.Enable {
|
||||||
inbound, err := s.GetInbound(id)
|
inbound, err := s.GetInbound(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
clients, err := s.GetClients(inbound)
|
clients, err := s.GetClients(inbound)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
if client.Email == clientEmail && client.Enable {
|
if client.Email == clientEmail && client.Enable {
|
||||||
rt, push, _, perr := s.nodePushPlan(inbound)
|
|
||||||
if perr != nil {
|
|
||||||
return false, perr
|
|
||||||
}
|
|
||||||
if !push {
|
|
||||||
if inbound.NodeID == nil {
|
|
||||||
needRestart = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
cipher := ""
|
cipher := ""
|
||||||
if string(inbound.Protocol) == "shadowsocks" {
|
if string(inbound.Protocol) == "shadowsocks" {
|
||||||
var oldSettings map[string]any
|
var oldSettings map[string]any
|
||||||
err = json.Unmarshal([]byte(inbound.Settings), &oldSettings)
|
err = json.Unmarshal([]byte(inbound.Settings), &oldSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
cipher, _ = oldSettings["method"].(string)
|
cipher, _ = oldSettings["method"].(string)
|
||||||
}
|
}
|
||||||
err1 := rt.AddUser(context.Background(), inbound, map[string]any{
|
clientMap := map[string]any{
|
||||||
"email": client.Email,
|
"email": client.Email,
|
||||||
"id": client.ID,
|
"id": client.ID,
|
||||||
"auth": client.Auth,
|
"auth": client.Auth,
|
||||||
@@ -634,14 +646,11 @@ func (s *InboundService) resetClientTrafficLocked(id int, clientEmail string) (b
|
|||||||
"flow": client.Flow,
|
"flow": client.Flow,
|
||||||
"password": client.Password,
|
"password": client.Password,
|
||||||
"cipher": cipher,
|
"cipher": cipher,
|
||||||
})
|
}
|
||||||
if err1 == nil {
|
if inbound.NodeID != nil {
|
||||||
logger.Debug("Client enabled on", rt.Name(), "due to reset traffic:", clientEmail)
|
reenableNodeID = inbound.NodeID
|
||||||
} else if inbound.NodeID != nil {
|
|
||||||
logger.Warning("Error in enabling client on", rt.Name(), ":", err1)
|
|
||||||
} else {
|
} else {
|
||||||
logger.Debug("Error in enabling client on", rt.Name(), ":", err1)
|
reenablePlan = &trafficLocalApplyPlan{action: trafficAddUser, inbound: *inbound, client: clientMap}
|
||||||
needRestart = true
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -656,7 +665,7 @@ func (s *InboundService) resetClientTrafficLocked(id int, clientEmail string) (b
|
|||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
inbound, err := s.GetInbound(id)
|
inbound, err := s.GetInbound(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
if err := adjustGroupBaselinesForRemovedTraffic(tx, []string{clientEmail}); err != nil {
|
if err := adjustGroupBaselinesForRemovedTraffic(tx, []string{clientEmail}); err != nil {
|
||||||
@@ -676,25 +685,30 @@ func (s *InboundService) resetClientTrafficLocked(id int, clientEmail string) (b
|
|||||||
Update("last_traffic_reset_time", now).Error; err != nil {
|
Update("last_traffic_reset_time", now).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if reenableNodeID != nil {
|
||||||
|
return (&NodeService{}).MarkNodeDirtyTx(tx, *reenableNodeID)
|
||||||
|
}
|
||||||
if inbound != nil && inbound.NodeID != nil {
|
if inbound != nil && inbound.NodeID != nil {
|
||||||
return (&NodeService{}).MarkNodeDirtyTx(tx, *inbound.NodeID)
|
return (&NodeService{}).MarkNodeDirtyTx(tx, *inbound.NodeID)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return false, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if inbound != nil && inbound.NodeID != nil {
|
if reenablePlan != nil {
|
||||||
if rt, rterr := s.runtimeFor(inbound); rterr == nil {
|
rt, err := s.runtimeFor(&reenablePlan.inbound)
|
||||||
if e := rt.ResetClientTraffic(context.Background(), inbound, clientEmail); e != nil {
|
if err != nil {
|
||||||
logger.Warning("ResetClientTraffic: remote propagation to", rt.Name(), "failed:", e)
|
needRestart = true
|
||||||
}
|
} else if err := rt.AddUser(context.Background(), &reenablePlan.inbound, reenablePlan.client); err != nil {
|
||||||
|
logger.Debug("Error in enabling client on", rt.Name(), ":", err)
|
||||||
|
needRestart = true
|
||||||
} else {
|
} else {
|
||||||
logger.Warning("ResetClientTraffic: runtime lookup failed:", rterr)
|
logger.Debug("Client enabled on", rt.Name(), "due to reset traffic:", clientEmail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return needRestart, nil
|
return needRestart, inbound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) ResetAllTraffics() error {
|
func (s *InboundService) ResetAllTraffics() error {
|
||||||
@@ -740,16 +754,24 @@ func (s *InboundService) propagateResetAllTrafficsToNodes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) ResetInboundTraffic(id int) error {
|
func (s *InboundService) ResetInboundTraffic(id int) error {
|
||||||
|
var inbound *model.Inbound
|
||||||
if err := submitTrafficWrite(func() error {
|
if err := submitTrafficWrite(func() error {
|
||||||
return database.GetDB().Model(model.Inbound{}).
|
db := database.GetDB()
|
||||||
|
if err := db.Model(model.Inbound{}).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Updates(map[string]any{"up": 0, "down": 0}).Error
|
Updates(map[string]any{"up": 0, "down": 0}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
inbound, err = s.GetInbound(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if inbound != nil && inbound.NodeID != nil {
|
||||||
inbound, err := s.GetInbound(id)
|
|
||||||
if err == nil && inbound != nil && inbound.NodeID != nil {
|
|
||||||
if rt, rterr := s.runtimeFor(inbound); rterr == nil {
|
if rt, rterr := s.runtimeFor(inbound); rterr == nil {
|
||||||
if e := rt.ResetInboundTraffic(context.Background(), inbound); e != nil {
|
if e := rt.ResetInboundTraffic(context.Background(), inbound); e != nil {
|
||||||
logger.Warning("ResetInboundTraffic: remote propagation to", rt.Name(), "failed:", e)
|
logger.Warning("ResetInboundTraffic: remote propagation to", rt.Name(), "failed:", e)
|
||||||
@@ -763,134 +785,161 @@ func (s *InboundService) ResetInboundTraffic(id int) error {
|
|||||||
|
|
||||||
func (s *InboundService) DelDepletedClients(id int) (err error) {
|
func (s *InboundService) DelDepletedClients(id int) (err error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
tx := db.Begin()
|
var deletedInbounds []model.Inbound
|
||||||
defer func() {
|
err = db.Transaction(func(tx *gorm.DB) error {
|
||||||
if err == nil {
|
// Collect depleted emails globally — a shared-email row owned by one
|
||||||
tx.Commit()
|
// inbound depletes every sibling that lists the email.
|
||||||
} else {
|
now := time.Now().Unix() * 1000
|
||||||
tx.Rollback()
|
depletedClause := "reset = 0 and ((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?))"
|
||||||
|
var depletedRows []xray.ClientTraffic
|
||||||
|
if err := tx.Model(xray.ClientTraffic{}).
|
||||||
|
Where(depletedClause, now).
|
||||||
|
Find(&depletedRows).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(depletedRows) == 0 {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
// Collect depleted emails globally — a shared-email row owned by one
|
depletedEmails := make(map[string]struct{}, len(depletedRows))
|
||||||
// inbound depletes every sibling that lists the email.
|
for _, r := range depletedRows {
|
||||||
now := time.Now().Unix() * 1000
|
if r.Email == "" {
|
||||||
depletedClause := "reset = 0 and ((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?))"
|
continue
|
||||||
var depletedRows []xray.ClientTraffic
|
}
|
||||||
err = db.Model(xray.ClientTraffic{}).
|
depletedEmails[strings.ToLower(r.Email)] = struct{}{}
|
||||||
Where(depletedClause, now).
|
}
|
||||||
Find(&depletedRows).Error
|
if len(depletedEmails) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var inbounds []*model.Inbound
|
||||||
|
inboundQuery := tx.Model(model.Inbound{})
|
||||||
|
if id >= 0 {
|
||||||
|
inboundQuery = inboundQuery.Where("id = ?", id)
|
||||||
|
}
|
||||||
|
if err := inboundQuery.Find(&inbounds).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
var settings map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rawClients, ok := settings["clients"].([]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newClients := make([]any, 0, len(rawClients))
|
||||||
|
removed := 0
|
||||||
|
for _, client := range rawClients {
|
||||||
|
c, ok := client.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
newClients = append(newClients, client)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
email, _ := c["email"].(string)
|
||||||
|
if _, isDepleted := depletedEmails[strings.ToLower(email)]; isDepleted {
|
||||||
|
removed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newClients = append(newClients, client)
|
||||||
|
}
|
||||||
|
if removed == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(newClients) == 0 {
|
||||||
|
deletedInbounds = append(deletedInbounds, *inbound)
|
||||||
|
if err := s.clientService.DetachInbound(tx, inbound.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Where("inbound_id = ?", inbound.Id).Delete(&model.Host{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Delete(model.Inbound{}, inbound.Id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if inbound.NodeID != nil {
|
||||||
|
if err := (&NodeService{}).MarkNodeDirtyTx(tx, *inbound.NodeID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
settings["clients"] = newClients
|
||||||
|
ns, mErr := json.MarshalIndent(settings, "", " ")
|
||||||
|
if mErr != nil {
|
||||||
|
return mErr
|
||||||
|
}
|
||||||
|
inbound.Settings = string(ns)
|
||||||
|
if err := tx.Save(inbound).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
survivingClients, gcErr := s.GetClients(inbound)
|
||||||
|
if gcErr != nil {
|
||||||
|
return gcErr
|
||||||
|
}
|
||||||
|
if err := s.clientService.SyncInbound(tx, inbound.Id, survivingClients); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if inbound.NodeID != nil {
|
||||||
|
if err := (&NodeService{}).MarkNodeDirtyTx(tx, *inbound.NodeID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop now-orphaned rows. With id >= 0, a row is safe to drop only when
|
||||||
|
// no out-of-scope inbound still references the email.
|
||||||
|
if id < 0 {
|
||||||
|
return tx.Where(depletedClause, now).Delete(xray.ClientTraffic{}).Error
|
||||||
|
}
|
||||||
|
emails := make([]string, 0, len(depletedEmails))
|
||||||
|
for e := range depletedEmails {
|
||||||
|
emails = append(emails, e)
|
||||||
|
}
|
||||||
|
var stillReferenced []string
|
||||||
|
emailExpr := database.JSONFieldText("client.value", "email")
|
||||||
|
stillQuery := fmt.Sprintf(
|
||||||
|
"SELECT DISTINCT LOWER(%s) %s WHERE LOWER(%s) IN ?",
|
||||||
|
emailExpr,
|
||||||
|
database.JSONClientsFromInbound(),
|
||||||
|
emailExpr,
|
||||||
|
)
|
||||||
|
if err := tx.Raw(stillQuery, emails).Scan(&stillReferenced).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stillSet := make(map[string]struct{}, len(stillReferenced))
|
||||||
|
for _, e := range stillReferenced {
|
||||||
|
stillSet[e] = struct{}{}
|
||||||
|
}
|
||||||
|
toDelete := make([]string, 0, len(emails))
|
||||||
|
for _, e := range emails {
|
||||||
|
if _, kept := stillSet[e]; !kept {
|
||||||
|
toDelete = append(toDelete, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(toDelete) > 0 {
|
||||||
|
if err := tx.Where("LOWER(email) IN ?", toDelete).Delete(xray.ClientTraffic{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(depletedRows) == 0 {
|
for i := range deletedInbounds {
|
||||||
return nil
|
inbound := &deletedInbounds[i]
|
||||||
}
|
if rt, rtErr := s.runtimeFor(inbound); rtErr != nil {
|
||||||
|
logger.Warning("DelDepletedClients: runtime lookup failed after commit:", rtErr)
|
||||||
depletedEmails := make(map[string]struct{}, len(depletedRows))
|
} else if rtErr = rt.DelInbound(context.Background(), inbound); rtErr != nil && !xray.IsMissingHandlerErr(rtErr) {
|
||||||
for _, r := range depletedRows {
|
logger.Warning("DelDepletedClients: runtime cleanup failed after commit:", rtErr)
|
||||||
if r.Email == "" {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
depletedEmails[strings.ToLower(r.Email)] = struct{}{}
|
if inbound.Tag != "" {
|
||||||
}
|
if _, syncErr := (&XraySettingService{}).RemoveInboundTagReferences(inbound.Tag); syncErr != nil {
|
||||||
if len(depletedEmails) == 0 {
|
logger.Warning("DelDepletedClients: routing cleanup failed after commit:", syncErr)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var inbounds []*model.Inbound
|
|
||||||
inboundQuery := db.Model(model.Inbound{})
|
|
||||||
if id >= 0 {
|
|
||||||
inboundQuery = inboundQuery.Where("id = ?", id)
|
|
||||||
}
|
|
||||||
if err = inboundQuery.Find(&inbounds).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, inbound := range inbounds {
|
|
||||||
var settings map[string]any
|
|
||||||
if err = json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rawClients, ok := settings["clients"].([]any)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newClients := make([]any, 0, len(rawClients))
|
|
||||||
removed := 0
|
|
||||||
for _, client := range rawClients {
|
|
||||||
c, ok := client.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
newClients = append(newClients, client)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
email, _ := c["email"].(string)
|
|
||||||
if _, isDepleted := depletedEmails[strings.ToLower(email)]; isDepleted {
|
|
||||||
removed++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newClients = append(newClients, client)
|
|
||||||
}
|
|
||||||
if removed == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(newClients) == 0 {
|
|
||||||
_, _ = s.DelInbound(inbound.Id)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
settings["clients"] = newClients
|
|
||||||
ns, mErr := json.MarshalIndent(settings, "", " ")
|
|
||||||
if mErr != nil {
|
|
||||||
return mErr
|
|
||||||
}
|
|
||||||
inbound.Settings = string(ns)
|
|
||||||
if err = tx.Save(inbound).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
survivingClients, gcErr := s.GetClients(inbound)
|
|
||||||
if gcErr != nil {
|
|
||||||
err = gcErr
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = s.clientService.SyncInbound(tx, inbound.Id, survivingClients); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drop now-orphaned rows. With id >= 0, a row is safe to drop only when
|
|
||||||
// no out-of-scope inbound still references the email.
|
|
||||||
if id < 0 {
|
|
||||||
err = tx.Where(depletedClause, now).Delete(xray.ClientTraffic{}).Error
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
emails := make([]string, 0, len(depletedEmails))
|
|
||||||
for e := range depletedEmails {
|
|
||||||
emails = append(emails, e)
|
|
||||||
}
|
|
||||||
var stillReferenced []string
|
|
||||||
emailExpr := database.JSONFieldText("client.value", "email")
|
|
||||||
stillQuery := fmt.Sprintf(
|
|
||||||
"SELECT DISTINCT LOWER(%s) %s WHERE LOWER(%s) IN ?",
|
|
||||||
emailExpr,
|
|
||||||
database.JSONClientsFromInbound(),
|
|
||||||
emailExpr,
|
|
||||||
)
|
|
||||||
if err = tx.Raw(stillQuery, emails).Scan(&stillReferenced).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
stillSet := make(map[string]struct{}, len(stillReferenced))
|
|
||||||
for _, e := range stillReferenced {
|
|
||||||
stillSet[e] = struct{}{}
|
|
||||||
}
|
|
||||||
toDelete := make([]string, 0, len(emails))
|
|
||||||
for _, e := range emails {
|
|
||||||
if _, kept := stillSet[e]; !kept {
|
|
||||||
toDelete = append(toDelete, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(toDelete) > 0 {
|
|
||||||
if err = tx.Where("LOWER(email) IN ?", toDelete).Delete(xray.ClientTraffic{}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type trafficLocalApplyAction uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
trafficAddUser trafficLocalApplyAction = iota + 1
|
||||||
|
trafficRemoveUser
|
||||||
|
trafficDisableInbound
|
||||||
|
)
|
||||||
|
|
||||||
|
type trafficLocalApplyPlan struct {
|
||||||
|
action trafficLocalApplyAction
|
||||||
|
inbound model.Inbound
|
||||||
|
client map[string]any
|
||||||
|
email string
|
||||||
|
}
|
||||||
|
|
||||||
|
type trafficMutationBatch struct {
|
||||||
|
localPlans []trafficLocalApplyPlan
|
||||||
|
remotePlans []trafficInboundUpdatePlan
|
||||||
|
nodeIDs map[int]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type trafficInboundUpdatePlan struct{ oldInbound, newInbound model.Inbound }
|
||||||
|
|
||||||
|
func newTrafficMutationBatch() *trafficMutationBatch {
|
||||||
|
return &trafficMutationBatch{nodeIDs: make(map[int]struct{})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *trafficMutationBatch) addNode(nodeID int) {
|
||||||
|
if nodeID > 0 {
|
||||||
|
b.nodeIDs[nodeID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *trafficMutationBatch) markNodesTx(tx *gorm.DB) error {
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
nodeSvc := NodeService{}
|
||||||
|
for nodeID := range b.nodeIDs {
|
||||||
|
if err := nodeSvc.MarkNodeDirtyTx(tx, nodeID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) applyTrafficMutationBatch(b *trafficMutationBatch) bool {
|
||||||
|
if b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
needRestart := false
|
||||||
|
for i := range b.remotePlans {
|
||||||
|
plan := &b.remotePlans[i]
|
||||||
|
rt, err := s.runtimeFor(&plan.newInbound)
|
||||||
|
if err == nil {
|
||||||
|
err = rt.UpdateInbound(context.Background(), &plan.oldInbound, &plan.newInbound)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug("traffic post-commit remote apply failed:", err)
|
||||||
|
needRestart = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range b.localPlans {
|
||||||
|
plan := &b.localPlans[i]
|
||||||
|
if plan.inbound.Protocol == model.MTProto {
|
||||||
|
s.applyLocalMtproto(plan.inbound.Id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rt, err := s.runtimeFor(&plan.inbound)
|
||||||
|
if err == nil {
|
||||||
|
switch plan.action {
|
||||||
|
case trafficAddUser:
|
||||||
|
err = rt.AddUser(context.Background(), &plan.inbound, plan.client)
|
||||||
|
case trafficRemoveUser:
|
||||||
|
err = rt.RemoveUser(context.Background(), &plan.inbound, plan.email)
|
||||||
|
if err != nil && strings.Contains(err.Error(), "not found") {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
case trafficDisableInbound:
|
||||||
|
err = rt.DelInbound(context.Background(), &plan.inbound)
|
||||||
|
if xray.IsMissingHandlerErr(err) {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug("traffic post-commit runtime apply failed:", err)
|
||||||
|
needRestart = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return needRestart
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTrafficDisableImmediatelyUpdatesNodeRuntime(t *testing.T) {
|
||||||
|
setupConflictDB(t)
|
||||||
|
nodeID, fake := setupNodeRuntime(t)
|
||||||
|
client := model.Client{Email: "spent-node", Enable: true}
|
||||||
|
ib := nodeInbound(t, nodeID, 46301, []model.Client{client})
|
||||||
|
if err := database.GetDB().Create(&xray.ClientTraffic{
|
||||||
|
InboundId: ib.Id, Email: client.Email, Enable: true, Up: 100, Total: 100,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed traffic: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, _, _, err := (&InboundService{}).addTrafficLocked(nil, nil); err != nil {
|
||||||
|
t.Fatalf("addTrafficLocked: %v", err)
|
||||||
|
}
|
||||||
|
if got := fake.updateInbound.Load(); got != 1 {
|
||||||
|
t.Fatalf("remote UpdateInbound calls = %d, want 1 after commit", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrafficDisableRefreshesLocalMTProtoSidecar(t *testing.T) {
|
||||||
|
setupConflictDB(t)
|
||||||
|
mgr := runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }})
|
||||||
|
fake := &fakeNodeRuntime{}
|
||||||
|
mgr.SetLocalRuntimeOverride(fake)
|
||||||
|
runtime.SetManager(mgr)
|
||||||
|
t.Cleanup(func() { runtime.SetManager(nil) })
|
||||||
|
|
||||||
|
seedInboundConflict(t, "mt-spent", "", 46302, model.MTProto, "",
|
||||||
|
`{"clients":[{"email":"spent-mt","secret":"`+mtprotoTestSecretA+`","enable":true}]}`)
|
||||||
|
ib := loadInboundByTag(t, "mt-spent")
|
||||||
|
clients, err := (&InboundService{}).GetClients(ib)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetClients: %v", err)
|
||||||
|
}
|
||||||
|
if err := (&ClientService{}).SyncInbound(nil, ib.Id, clients); err != nil {
|
||||||
|
t.Fatalf("SyncInbound: %v", err)
|
||||||
|
}
|
||||||
|
seedClientTraffic(t, ib.Id, "spent-mt", true)
|
||||||
|
if err := database.GetDB().Model(&xray.ClientTraffic{}).Where("email = ?", "spent-mt").
|
||||||
|
Updates(map[string]any{"up": 100, "total": 100}).Error; err != nil {
|
||||||
|
t.Fatalf("deplete traffic: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, _, _, err := (&InboundService{}).addTrafficLocked(nil, nil); err != nil {
|
||||||
|
t.Fatalf("addTrafficLocked: %v", err)
|
||||||
|
}
|
||||||
|
if got := fake.updateInbound.Load(); got != 1 {
|
||||||
|
t.Fatalf("MTProto sidecar UpdateInbound calls = %d, want 1 after commit", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelDepletedClientsCleansRuntimeAfterCommit(t *testing.T) {
|
||||||
|
setupConflictDB(t)
|
||||||
|
mgr := runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }})
|
||||||
|
fake := &fakeNodeRuntime{}
|
||||||
|
mgr.SetLocalRuntimeOverride(fake)
|
||||||
|
runtime.SetManager(mgr)
|
||||||
|
t.Cleanup(func() { runtime.SetManager(nil) })
|
||||||
|
|
||||||
|
seedInboundConflict(t, "depleted-only", "", 46303, model.VLESS, `{"network":"tcp"}`,
|
||||||
|
`{"clients":[{"email":"gone","enable":true}]}`)
|
||||||
|
ib := loadInboundByTag(t, "depleted-only")
|
||||||
|
seedClientTraffic(t, ib.Id, "gone", true)
|
||||||
|
if err := database.GetDB().Model(&xray.ClientTraffic{}).Where("email = ?", "gone").
|
||||||
|
Updates(map[string]any{"up": 100, "total": 100, "reset": 0}).Error; err != nil {
|
||||||
|
t.Fatalf("deplete traffic: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := (&InboundService{}).DelDepletedClients(-1); err != nil {
|
||||||
|
t.Fatalf("DelDepletedClients: %v", err)
|
||||||
|
}
|
||||||
|
if got := fake.delInbound.Load(); got != 1 {
|
||||||
|
t.Fatalf("runtime DelInbound calls = %d, want 1 after commit", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user