diff --git a/internal/web/service/inbound.go b/internal/web/service/inbound.go index a0296cb8f..50a875721 100644 --- a/internal/web/service/inbound.go +++ b/internal/web/service/inbound.go @@ -1574,17 +1574,14 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo return perr } if push { - payload := inbound - pushable := true - if inbound.Protocol == model.MTProto || inbound.Protocol == model.TUIC { - if built, bErr := s.buildInboundForLocalRuntime(tx, inbound); bErr == nil { - payload = built - } else { - logger.Debug("Unable to prepare runtime inbound config:", bErr) - pushable = false + // An imported or master-pushed row lists its disabled and depleted clients too. + payload, bErr := s.buildInboundForLocalRuntime(tx, inbound) + if bErr != nil { + logger.Debug("Unable to prepare runtime inbound config:", bErr) + if inbound.Protocol != model.MTProto && inbound.Protocol != model.TUIC { + needRestart = true } - } - if pushable { + } else { postCommitApply = func() { if err1 := rt.AddInbound(context.Background(), payload); err1 == nil { logger.Debug("New inbound added on", rt.Name(), ":", inbound.Tag) diff --git a/internal/web/service/inbound_update_hot_users_test.go b/internal/web/service/inbound_update_hot_users_test.go index 329f81480..ec26c284b 100644 --- a/internal/web/service/inbound_update_hot_users_test.go +++ b/internal/web/service/inbound_update_hot_users_test.go @@ -11,6 +11,7 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" + "github.com/mhsanaei/3x-ui/v3/internal/xray" ) // inboundOpsProbe records every handler and user operation the panel sends the @@ -322,3 +323,55 @@ func TestNodeShadowsocksPushChangingOnlyClientsKeepsTheHandler(t *testing.T) { } } } + +// addInboundRecorder keeps the payload handed to the local runtime, so a test can +// check what the running core would register rather than what the DB stores. +type addInboundRecorder struct { + fakeNodeRuntime + added *model.Inbound +} + +func (r *addInboundRecorder) AddInbound(_ context.Context, ib *model.Inbound) error { + r.added = ib + return nil +} + +// An imported or master-pushed inbound arrives with its clients, switched-off +// ones included; only UpdateInbound/SetInboundEnable used to strip them. +func TestAddInboundKeepsDisabledClientsOutOfTheCore(t *testing.T) { + setupConflictDB(t) + mgr := useTestRuntimeManager(t) + local := &addInboundRecorder{} + mgr.SetLocalRuntimeOverride(local) + + svc := &InboundService{} + saved, needRestart, err := svc.AddInbound(&model.Inbound{ + UserId: 1, Enable: true, Port: 44490, Protocol: model.VLESS, + StreamSettings: `{"network":"tcp","security":"none"}`, + Settings: `{"decryption":"none","clients":[ + {"email":"paying","id":"5f2eb9d6-3a2f-4a55-9812-6ea1e2f7a001","enable":true}, + {"email":"banned","id":"5f2eb9d6-3a2f-4a55-9812-6ea1e2f7a002","enable":false}, + {"email":"depleted","id":"5f2eb9d6-3a2f-4a55-9812-6ea1e2f7a003","enable":true}]}`, + ClientStats: []xray.ClientTraffic{{Email: "depleted", Enable: false, Up: 10, Total: 10}}, + }) + if err != nil { + t.Fatalf("AddInbound: %v", err) + } + if needRestart || local.added == nil { + t.Fatalf("the inbound must be hot-added to the running core; needRestart=%v added=%v", needRestart, local.added) + } + served, err := svc.GetClients(local.added) + if err != nil { + t.Fatalf("GetClients(runtime payload): %v", err) + } + if len(served) != 1 || served[0].Email != "paying" { + t.Fatalf("the core must get only the enabled client, got %s", local.added.Settings) + } + stored, err := svc.GetClients(saved) + if err != nil { + t.Fatalf("GetClients(stored row): %v", err) + } + if len(stored) != 3 { + t.Fatalf("the stored row must keep every client, got %s", saved.Settings) + } +}