mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
fix(inbounds): serve fresh client UUIDs for list and allLinks (#6458)
* fix(inbounds): serve fresh client UUIDs for list and allLinks (#6436) Resolve clients from the clients table in inboundLinks and backfillClientStats so /inbounds/list ClientStats and allLinks match the running Xray identity when embedded settings JSON is stale. * fix(sub): keep WG/AWG settings identity in link exports (#6436) clientsForLinkExport uses the clients table for UUID-bearing protocols and the inbound settings JSON for WireGuard/AmneziaWG so allLinks and per-client QR links stay consistent without collapsing per-inbound tunnel keys. * fix(sub): fall back to settings clients for link export (#6458) Prefer ListClientsForInbound for UUID protocols, but when the clients table is empty or unavailable fall back to GetClients so settings-only inbounds (and share-link unit tests) still produce links. Keep WG/AWG on settings identity. --------- Co-authored-by: mrchatam <mrchatam@users.noreply.github.com> Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com>
This commit is contained in:
@@ -573,6 +573,14 @@ func (s *InboundService) GetClientsBySubId(inboundId int, subId string) ([]model
|
||||
return s.clientService.ListForInboundBySubId(nil, inboundId, subId)
|
||||
}
|
||||
|
||||
// ListClientsForInbound returns every client attached to the inbound from the
|
||||
// normalized clients tables — the same source the running Xray config uses —
|
||||
// instead of parsing the embedded settings JSON, which can hold a stale UUID
|
||||
// after a client identity change (#6436).
|
||||
func (s *InboundService) ListClientsForInbound(inboundId int) ([]model.Client, error) {
|
||||
return s.clientService.ListForInbound(nil, inboundId)
|
||||
}
|
||||
|
||||
func (s *InboundService) GetAllEmails() ([]string, error) {
|
||||
db := database.GetDB()
|
||||
var emails []string
|
||||
|
||||
@@ -24,10 +24,12 @@ type CopyClientsResult struct {
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
// enrichClientStats parses each inbound's clients once, fills in the
|
||||
// UUID/SubId fields on the preloaded ClientStats, and tops up rows owned by
|
||||
// a sibling inbound (shared-email mode — the row is keyed on email so it
|
||||
// only preloads on its owning inbound).
|
||||
// enrichClientStats resolves each inbound's clients from the clients table
|
||||
// once, fills in the UUID/SubId fields on the preloaded ClientStats, and tops
|
||||
// up rows owned by a sibling inbound (shared-email mode — the row is keyed on
|
||||
// email so it only preloads on its owning inbound). Reading identity from the
|
||||
// clients table keeps /inbounds/list in sync with the running Xray config
|
||||
// when the embedded settings JSON is stale (#6436).
|
||||
func (s *InboundService) enrichClientStats(db *gorm.DB, inbounds []*model.Inbound) {
|
||||
if len(inbounds) == 0 {
|
||||
return
|
||||
@@ -55,13 +57,14 @@ func (s *InboundService) enrichClientStats(db *gorm.DB, inbounds []*model.Inboun
|
||||
// backfillClientStats tops up each inbound's preloaded ClientStats with rows
|
||||
// owned by a sibling inbound: client_traffics is keyed on email, so a client
|
||||
// attached to several inbounds has one row that only preloads on the inbound
|
||||
// it was created on. Returns the parsed clients per inbound for reuse.
|
||||
// it was created on. Returns the clients-table clients per inbound for reuse
|
||||
// (not the embedded settings JSON, which can lag the live UUID — #6436).
|
||||
func (s *InboundService) backfillClientStats(db *gorm.DB, inbounds []*model.Inbound) [][]model.Client {
|
||||
clientsByInbound := make([][]model.Client, len(inbounds))
|
||||
seenByInbound := make([]map[string]struct{}, len(inbounds))
|
||||
missing := make(map[string]struct{})
|
||||
for i, inbound := range inbounds {
|
||||
clients, _ := s.GetClients(inbound)
|
||||
clients, _ := s.clientService.ListForInbound(db, inbound.Id)
|
||||
clientsByInbound[i] = clients
|
||||
seen := make(map[string]struct{}, len(inbound.ClientStats))
|
||||
for _, st := range inbound.ClientStats {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
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/xray"
|
||||
)
|
||||
|
||||
// GetInbounds must enrich ClientStats.UUID from the clients table, not the
|
||||
// embedded inbound settings JSON, when the two diverge (#6436).
|
||||
func TestEnrichClientStats_UsesClientsTableUUIDWhenSettingsStale(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
db := database.GetDB()
|
||||
stale := "11111111-1111-1111-1111-111111111111"
|
||||
fresh := "22222222-2222-2222-2222-222222222222"
|
||||
|
||||
ib := &model.Inbound{
|
||||
UserId: 1, Tag: "stale-uuid-list", Enable: true, Listen: "0.0.0.0", Port: 8443,
|
||||
Protocol: model.VLESS, Remark: "stale",
|
||||
Settings: `{"clients":[{"id":"` + stale + `","email":"stale@e","subId":"sub1","enable":true}],"decryption":"none"}`,
|
||||
StreamSettings: `{"network":"tcp","security":"none"}`,
|
||||
}
|
||||
if err := db.Create(ib).Error; err != nil {
|
||||
t.Fatalf("create inbound: %v", err)
|
||||
}
|
||||
rec := &model.ClientRecord{Email: "stale@e", SubID: "sub1", UUID: fresh, Enable: true}
|
||||
if err := db.Create(rec).Error; err != nil {
|
||||
t.Fatalf("create client: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.ClientInbound{ClientId: rec.Id, InboundId: ib.Id}).Error; err != nil {
|
||||
t.Fatalf("create client_inbound: %v", err)
|
||||
}
|
||||
if err := db.Create(&xray.ClientTraffic{InboundId: ib.Id, Email: "stale@e", Enable: true}).Error; err != nil {
|
||||
t.Fatalf("create client_traffics: %v", err)
|
||||
}
|
||||
|
||||
svc := &InboundService{}
|
||||
inbounds, err := svc.GetInbounds(1)
|
||||
if err != nil {
|
||||
t.Fatalf("GetInbounds: %v", err)
|
||||
}
|
||||
if len(inbounds) != 1 {
|
||||
t.Fatalf("inbounds = %d, want 1", len(inbounds))
|
||||
}
|
||||
if len(inbounds[0].ClientStats) != 1 {
|
||||
t.Fatalf("ClientStats = %d, want 1", len(inbounds[0].ClientStats))
|
||||
}
|
||||
got := inbounds[0].ClientStats[0].UUID
|
||||
if got != fresh {
|
||||
t.Fatalf("ClientStats.UUID = %q, want fresh %q (settings still had stale %q)", got, fresh, stale)
|
||||
}
|
||||
if got == stale {
|
||||
t.Fatalf("ClientStats.UUID still carries stale settings value %q", stale)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user