Kaynağa Gözat

fix(clients): use EffectiveFlow in BulkAttach (#6454)

* fix(clients): use EffectiveFlow in BulkAttach

Mirror Attach (#4834): seed wire clients from EffectiveFlowsByEmails so a zeroed clients.flow column does not drop Vision on bulk attach (#6432).

* test(clients): cover BulkAttach Vision flow when clients.flow is zeroed

Regression for #6432 — same scenario as TestAttach_PreservesVisionFlowWhenCanonicalColumnZeroed.

* fix(clients): only apply EffectiveFlow when present in BulkAttach

Avoid overwriting a non-empty clients.flow when EffectiveFlowsByEmails
has no entry for the email. Also drop the extra blank line that broke gofumpt.

---------

Co-authored-by: mrchatam <[email protected]>
mrchatam 4 saat önce
ebeveyn
işleme
0a838563bb

+ 14 - 0
internal/web/service/client_bulk.go

@@ -60,6 +60,17 @@ func (s *ClientService) BulkAttach(inboundSvc *InboundService, emails []string,
 		records = append(records, rec)
 	}
 
+	// Same rule as Attach (#4834): clients.flow is unreliable when a non-flow
+	// inbound synced last, so seed from EffectiveFlow before clientWithInboundFlow.
+	emailsForFlow := make([]string, 0, len(records))
+	for _, rec := range records {
+		emailsForFlow = append(emailsForFlow, rec.Email)
+	}
+	flowsByEmail, err := s.EffectiveFlowsByEmails(nil, emailsForFlow)
+	if err != nil {
+		return result, false, err
+	}
+
 	needRestart := false
 	// Prepared in order first, as in Create: fillProtocolDefaults mints the
 	// shared credentials, so only the node pushes below may overlap.
@@ -100,6 +111,9 @@ func (s *ClientService) BulkAttach(inboundSvc *InboundService, emails []string,
 				continue
 			}
 			client := *rec.ToClient()
+			if flow, ok := flowsByEmail[rec.Email]; ok && flow != "" {
+				client.Flow = flow
+			}
 			client.UpdatedAt = time.Now().UnixMilli()
 			if err := s.fillProtocolDefaults(&client, inbound); err != nil {
 				recordErr("%s -> inbound %d: %v", rec.Email, ibId, err)

+ 76 - 0
internal/web/service/client_flow_isolation_test.go

@@ -263,3 +263,79 @@ func TestAttach_PreservesVisionFlowWhenCanonicalColumnZeroed(t *testing.T) {
 		t.Errorf("attached non-flow inbound must not receive Vision flow, got %#v", wsList)
 	}
 }
+
+func TestBulkAttach_PreservesVisionFlowWhenCanonicalColumnZeroed(t *testing.T) {
+	dbDir := t.TempDir()
+	t.Setenv("XUI_DB_FOLDER", dbDir)
+	if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
+		t.Fatalf("InitDB: %v", err)
+	}
+	t.Cleanup(func() { _ = database.CloseDB() })
+
+	db := database.GetDB()
+
+	const email = "[email protected]"
+	const uid = "ce8d33df-3a64-4f10-8f9b-91c3a8e0c222"
+	const sub = "subvisionbulk0001"
+	const vision = "xtls-rprx-vision"
+	const realityStream = `{"network":"tcp","security":"reality"}`
+
+	svc := ClientService{}
+	source := model.Client{Email: email, ID: uid, SubID: sub, Enable: true, Flow: vision}
+
+	reality1 := &model.Inbound{
+		Tag: "vless-reality-bulk-1", Enable: true, Port: 42101, Protocol: model.VLESS,
+		StreamSettings: realityStream,
+		Settings:       clientsSettings(t, []model.Client{source}),
+	}
+	if err := db.Create(reality1).Error; err != nil {
+		t.Fatalf("create reality1: %v", err)
+	}
+	reality2 := &model.Inbound{
+		Tag: "vless-reality-bulk-2", Enable: true, Port: 42102, Protocol: model.VLESS,
+		StreamSettings: realityStream, Settings: `{"clients":[]}`,
+	}
+	if err := db.Create(reality2).Error; err != nil {
+		t.Fatalf("create reality2: %v", err)
+	}
+	wsTls := &model.Inbound{
+		Tag: "vless-ws-bulk", Enable: true, Port: 42103, Protocol: model.VLESS,
+		StreamSettings: `{"network":"ws","security":"tls"}`, Settings: `{"clients":[]}`,
+	}
+	if err := db.Create(wsTls).Error; err != nil {
+		t.Fatalf("create ws: %v", err)
+	}
+
+	if err := svc.SyncInbound(nil, reality1.Id, []model.Client{clientWithInboundFlow(source, reality1)}); err != nil {
+		t.Fatalf("SyncInbound(reality1): %v", err)
+	}
+
+	rec, err := svc.GetRecordByEmail(nil, email)
+	if err != nil {
+		t.Fatalf("GetRecordByEmail: %v", err)
+	}
+	if err := db.Model(&model.ClientRecord{}).Where("id = ?", rec.Id).Update("flow", "").Error; err != nil {
+		t.Fatalf("zero canonical flow: %v", err)
+	}
+
+	inboundSvc := &InboundService{}
+	if _, _, err := svc.BulkAttach(inboundSvc, []string{email}, []int{reality2.Id, wsTls.Id}); err != nil {
+		t.Fatalf("BulkAttach: %v", err)
+	}
+
+	reality2List, err := svc.ListForInbound(nil, reality2.Id)
+	if err != nil {
+		t.Fatalf("ListForInbound(reality2): %v", err)
+	}
+	if len(reality2List) != 1 || reality2List[0].Flow != vision {
+		t.Errorf("bulk-attached flow-capable inbound must inherit Vision via EffectiveFlow (#6432), got %#v", reality2List)
+	}
+
+	wsList, err := svc.ListForInbound(nil, wsTls.Id)
+	if err != nil {
+		t.Fatalf("ListForInbound(ws): %v", err)
+	}
+	if len(wsList) != 1 || wsList[0].Flow != "" {
+		t.Errorf("bulk-attached non-flow inbound must not receive Vision flow, got %#v", wsList)
+	}
+}