fix(clients): parse only settings.clients across protocols (#5855)

* fix(clients): parse only settings.clients across protocols

Several inbound settings readers decoded the whole settings object into map[string][]model.Client. Real protocol settings include scalar keys such as VLESS decryption and Hysteria version, so that shape can fail before callers reach settings.clients or leave them relying on decoder side effects.

Add one shared helper that extracts only the clients field through json.RawMessage, then use it from GetClients, SearchClientTraffic and the IP-limit job fallback paths. Regression tests cover VLESS and Hysteria settings with scalar protocol fields.

* fix(clients): reject empty inbound settings
This commit is contained in:
n0ctal
2026-07-08 23:31:00 +05:00
committed by GitHub
parent 7db92d6318
commit 567a4ac4fe
6 changed files with 200 additions and 21 deletions
+4 -6
View File
@@ -357,9 +357,7 @@ func (j *CheckClientIpJob) processObserved(observed map[string]map[string]int64,
for _, d := range disconnects {
clients, cached := clientsCache[d.inbound.Id]
if !cached {
settings := map[string][]model.Client{}
_ = json.Unmarshal([]byte(d.inbound.Settings), &settings)
clients = settings["clients"]
clients, _ = service.ParseInboundSettingsClients(d.inbound.Settings)
clientsCache[d.inbound.Id] = clients
}
j.disconnectClientTemporarily(d.inbound, d.email, clients)
@@ -702,11 +700,11 @@ func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound
return nil, listErr
}
for i := range candidates {
settings := map[string][]model.Client{}
if jsonErr := json.Unmarshal([]byte(candidates[i].Settings), &settings); jsonErr != nil {
clients, jsonErr := service.ParseInboundSettingsClients(candidates[i].Settings)
if jsonErr != nil {
continue
}
for _, client := range settings["clients"] {
for _, client := range clients {
if client.Email == clientEmail {
return &candidates[i], nil
}
@@ -7,6 +7,9 @@ import (
"runtime"
"testing"
"time"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
func TestMergeClientIps_EvictsStaleOldEntries(t *testing.T) {
@@ -191,6 +194,40 @@ func TestPartitionLiveIps_ConcurrentLiveIpsSortedAscending(t *testing.T) {
}
}
func TestGetInboundByEmailFallbackIgnoresProtocolScalarFields(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() })
inbound := &model.Inbound{
UserId: 1,
Tag: "vless-limit-fallback",
Enable: true,
Port: 43002,
Protocol: model.VLESS,
Settings: `{
"clients": [{"email": "alice@example.test", "id": "11111111-1111-1111-1111-111111111111", "limitIp": 2}],
"decryption": "none",
"encryption": "none",
"fallbacks": []
}`,
}
if err := database.GetDB().Create(inbound).Error; err != nil {
t.Fatalf("create inbound: %v", err)
}
got, err := (&CheckClientIpJob{}).getInboundByEmail("alice@example.test")
if err != nil {
t.Fatalf("getInboundByEmail: %v", err)
}
if got.Id != inbound.Id {
t.Fatalf("inbound id = %d, want %d", got.Id, inbound.Id)
}
}
func TestPartitionLiveIps_EmptyScanLeavesDbIntact(t *testing.T) {
// quiet tick: nothing observed => nothing live. everything merged
// is historical. keeps the panel from wiping recent-but-idle ips.