Socks-family inbounds authenticate against settings.accounts, so the clients attached to them never reached the core: the generated config carried the settings' "clients" mirror (ignored by xray for these protocols, passwords and all) and only the inbound's own fixed account. Emit one account per enabled attached client instead — user = client email, pass = client password — appended after the inbound's stored accounts, which keep their order and win name collisions (managed-forwarding resolution reads the fixed account from the stored settings, untouched). Drop the clients mirror from the generated settings, and force auth=password when clients exist, since accounts are inert under noauth. Accounting needs no new plumbing: xray-core 26.6.27 reports these accounts under user>>><account user> counters (probe-verified with two accounts on both mixed and socks), and the agent snapshot -> report -> AddTraffic chain already keys client traffic by that exact string, so making the account name the client email aligns booking end to end. Clientless socks-family inbounds pass through verbatim.
159 lines
5.3 KiB
Go
159 lines
5.3 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
// seedSocksInbound homes a socks-family inbound (fixed landing account in
|
|
// its stored settings) on the shared clients-test agent node and attaches
|
|
// the given clients through the DB link tables, the same source the
|
|
// running config is built from.
|
|
func seedSocksInbound(t *testing.T, tag string, port int, protocol model.Protocol, settings string, clients []model.Client) {
|
|
t.Helper()
|
|
setupSettingTestDB(t)
|
|
db := database.GetDB()
|
|
nodeID := clientsTestNodeID(t, db)
|
|
in := &model.Inbound{
|
|
Tag: tag,
|
|
Enable: true,
|
|
Port: port,
|
|
Protocol: protocol,
|
|
Settings: settings,
|
|
NodeID: &nodeID,
|
|
}
|
|
if err := db.Create(in).Error; err != nil {
|
|
t.Fatalf("create socks inbound: %v", err)
|
|
}
|
|
svc := ClientService{}
|
|
if err := svc.SyncInbound(nil, in.Id, clients); err != nil {
|
|
t.Fatalf("SyncInbound: %v", err)
|
|
}
|
|
}
|
|
|
|
func emittedSettings(t *testing.T, tag string) map[string]any {
|
|
t.Helper()
|
|
svc := &XrayService{}
|
|
cfg, err := svc.GetXrayConfigForAgent(clientsTestNodeID(t, database.GetDB()), "", "")
|
|
if err != nil {
|
|
t.Fatalf("GetXrayConfigForAgent: %v", err)
|
|
}
|
|
for i := range cfg.InboundConfigs {
|
|
if cfg.InboundConfigs[i].Tag != tag {
|
|
continue
|
|
}
|
|
var s map[string]any
|
|
if err := json.Unmarshal([]byte(cfg.InboundConfigs[i].Settings), &s); err != nil {
|
|
t.Fatalf("unmarshal emitted settings: %v", err)
|
|
}
|
|
return s
|
|
}
|
|
t.Fatalf("inbound %q not found in generated config", tag)
|
|
return nil
|
|
}
|
|
|
|
func accountUsers(t *testing.T, settings map[string]any) []map[string]any {
|
|
t.Helper()
|
|
raw, ok := settings["accounts"].([]any)
|
|
if !ok {
|
|
t.Fatalf("settings.accounts must be an array, got %T", settings["accounts"])
|
|
}
|
|
out := make([]map[string]any, 0, len(raw))
|
|
for _, a := range raw {
|
|
m, ok := a.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("account entry must be an object, got %T", a)
|
|
}
|
|
out = append(out, m)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestGetXrayConfig_SocksClientsEmittedAsAccounts(t *testing.T) {
|
|
seedSocksInbound(t, "mixed-clients", 43201, model.Mixed,
|
|
`{"auth":"password","accounts":[{"user":"fixed@landing","pass":"fixed-pass"}],"udp":true}`,
|
|
[]model.Client{
|
|
{Email: "user1@x", Password: "pass-1", Enable: true},
|
|
{Email: "user2@x", Password: "pass-2", Enable: true},
|
|
{Email: "off@x", Password: "pass-3", Enable: true},
|
|
})
|
|
disableClients(t, "off@x")
|
|
|
|
settings := emittedSettings(t, "mixed-clients")
|
|
if _, ok := settings["clients"]; ok {
|
|
t.Fatal("settings.clients must not be emitted for a mixed inbound")
|
|
}
|
|
accounts := accountUsers(t, settings)
|
|
if len(accounts) != 3 {
|
|
t.Fatalf("expected fixed account + both enabled clients (disabled one excluded), got %d: %#v", len(accounts), accounts)
|
|
}
|
|
if accounts[0]["user"] != "fixed@landing" || accounts[0]["pass"] != "fixed-pass" {
|
|
t.Errorf("the inbound's own account must stay first and untouched: %#v", accounts[0])
|
|
}
|
|
if accounts[1]["user"] != "user1@x" || accounts[1]["pass"] != "pass-1" {
|
|
t.Errorf("client account must be keyed by email with the client password: %#v", accounts[1])
|
|
}
|
|
if accounts[2]["user"] != "user2@x" || accounts[2]["pass"] != "pass-2" {
|
|
t.Errorf("second client account must follow in order: %#v", accounts[2])
|
|
}
|
|
if settings["auth"] != "password" {
|
|
t.Errorf("auth must be password when clients are attached, got %v", settings["auth"])
|
|
}
|
|
}
|
|
|
|
func TestGetXrayConfig_SocksClientsForcePasswordAuth(t *testing.T) {
|
|
seedSocksInbound(t, "mixed-noauth", 43202, model.Mixed,
|
|
`{"auth":"noauth","udp":true}`,
|
|
[]model.Client{
|
|
{Email: "user1@x", Password: "pass-1", Enable: true},
|
|
})
|
|
|
|
settings := emittedSettings(t, "mixed-noauth")
|
|
accounts := accountUsers(t, settings)
|
|
if len(accounts) != 1 || accounts[0]["user"] != "user1@x" {
|
|
t.Fatalf("expected the single client account, got %#v", accounts)
|
|
}
|
|
if settings["auth"] != "password" {
|
|
t.Errorf("attaching clients must switch a noauth inbound to password auth, got %v", settings["auth"])
|
|
}
|
|
}
|
|
|
|
func TestGetXrayConfig_LegacySocksClientsEmittedAsAccounts(t *testing.T) {
|
|
seedSocksInbound(t, "socks-legacy", 43203, model.Protocol("socks"),
|
|
`{"auth":"password","accounts":[{"user":"fixed@landing","pass":"fixed-pass"}],"udp":true}`,
|
|
[]model.Client{
|
|
{Email: "user1@x", Password: "pass-1", Enable: true},
|
|
})
|
|
|
|
settings := emittedSettings(t, "socks-legacy")
|
|
accounts := accountUsers(t, settings)
|
|
if len(accounts) != 2 || accounts[1]["user"] != "user1@x" {
|
|
t.Fatalf("legacy socks rows must emit client accounts too, got %#v", accounts)
|
|
}
|
|
}
|
|
|
|
func TestGetXrayConfig_SocksWithoutClientsUntouched(t *testing.T) {
|
|
stored := `{"auth":"password","accounts":[{"user":"fixed@landing","pass":"fixed-pass"}],"udp":true}`
|
|
seedSocksInbound(t, "mixed-bare", 43204, model.Mixed, stored, nil)
|
|
|
|
svc := &XrayService{}
|
|
cfg, err := svc.GetXrayConfigForAgent(clientsTestNodeID(t, database.GetDB()), "", "")
|
|
if err != nil {
|
|
t.Fatalf("GetXrayConfigForAgent: %v", err)
|
|
}
|
|
for i := range cfg.InboundConfigs {
|
|
if cfg.InboundConfigs[i].Tag != "mixed-bare" {
|
|
continue
|
|
}
|
|
if string(cfg.InboundConfigs[i].Settings) != stored {
|
|
t.Fatalf("a clientless mixed inbound's settings must pass through verbatim:\nstored: %s\nemitted: %s",
|
|
stored, cfg.InboundConfigs[i].Settings)
|
|
}
|
|
return
|
|
}
|
|
t.Fatal("inbound mixed-bare not found in generated config")
|
|
}
|