瀏覽代碼

fix(web): remove deleted multi-inbound client from runtime regardless of shared email (#5543)

DelInboundClientByEmail gated the runtime RemoveUser/DeleteUser (and its
push-plan resolution) on !emailShared. But Xray users are keyed by inbound
tag + email, so a client attached to two inbounds left its user live in the
running Xray of every inbound where the email was still shared by a sibling
inbound, until an Xray restart.

Decouple the per-inbound runtime removal from emailShared; keep emailShared
only for preserving the shared email-keyed client_traffics/IP rows.
MHSanaei 10 小時之前
父節點
當前提交
896016f7f6
共有 2 個文件被更改,包括 42 次插入3 次删除
  1. 8 3
      internal/web/service/client_inbound_apply.go
  2. 34 0
      internal/web/service/del_shared_email_runtime_test.go

+ 8 - 3
internal/web/service/client_inbound_apply.go

@@ -787,9 +787,12 @@ func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inbo
 		delStat = traffic != nil
 	}
 
+	// The runtime user is scoped to this inbound's tag + email, so the push plan
+	// is resolved independently of emailShared — a sibling inbound still carrying
+	// the email must not suppress removing the user from this inbound's Xray.
 	var rt runtime.Runtime
 	var push bool
-	if len(email) > 0 && !emailShared && (oldInbound.NodeID != nil || needApiDel) {
+	if len(email) > 0 && (oldInbound.NodeID != nil || needApiDel) {
 		r, p, dirty, perr := inboundSvc.nodePushPlan(oldInbound)
 		if perr != nil {
 			return false, perr
@@ -828,8 +831,10 @@ func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inbo
 	}
 
 	// Apply the runtime delete after commit — outside the serialized writer so a
-	// slow node call can't stall traffic accounting.
-	if len(email) > 0 && !emailShared {
+	// slow node call can't stall traffic accounting. Independent of emailShared:
+	// Xray users are keyed by inbound tag, so the user must be removed from this
+	// inbound's runtime even when the same email survives in another inbound.
+	if len(email) > 0 {
 		if oldInbound.NodeID == nil {
 			// Local inbound: a disabled client isn't in the running Xray, so only
 			// a live one (needApiDel) needs an API removal.

+ 34 - 0
internal/web/service/del_shared_email_runtime_test.go

@@ -0,0 +1,34 @@
+package service
+
+import (
+	"testing"
+
+	"github.com/google/uuid"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+)
+
+// Deleting a client that is attached to more than one inbound must still remove
+// the user from the running runtime of the inbound being deleted from. The
+// runtime user is keyed by inbound tag, so a sibling inbound still carrying the
+// same email (emailShared) must not suppress the per-inbound runtime removal —
+// otherwise the deleted user keeps connecting on that inbound until Xray
+// restart (#5543).
+func TestDelInboundClientByEmail_SharedEmailStillRemovesFromRuntime(t *testing.T) {
+	setupBulkDB(t)
+	nodeID, fake := setupNodeRuntime(t)
+
+	shared := []model.Client{{ID: uuid.NewString(), Email: "shared@x", Enable: true}}
+	ibA := nodeInbound(t, nodeID, 31001, shared)
+	nodeInbound(t, nodeID, 31002, shared)
+
+	svc := &ClientService{}
+	inboundSvc := &InboundService{}
+
+	if _, err := svc.DelInboundClientByEmail(inboundSvc, ibA.Id, "shared@x", false); err != nil {
+		t.Fatalf("DelInboundClientByEmail: %v", err)
+	}
+
+	if got := fake.deleteUser.Load(); got != 1 {
+		t.Fatalf("shared-email delete dispatched %d DeleteUser RPCs, want 1 (must remove from the deleted inbound's runtime despite the sibling inbound) (#5543)", got)
+	}
+}