mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-05 06:42:14 +03:00
Exercising the whole XrayAPI surface against a real xray-core 26.7.28 (the version go.mod pins) turned up a way for ordinary panel activity to kill the core process, plus two smaller mismatches with what the core actually does. buildUserAccount picked the shadowsocks account type by falling through to a 2022 account whenever the cipher was not one of six hardcoded names. xray's legacy and 2022 inbounds cast the account they are handed without checking (proxy/shadowsocks/validator.go, proxy/shadowsocks_2022/inbound_multi.go), so the wrong type is not an error — it panics the core and drops every connection on the server. The fallback was reachable without any misconfiguration: autoRenewClients hands AddUser the client object straight out of the inbound's settings, where the cipher lives under "method", never "cipher", so every auto-renewed client on a legacy-cipher shadowsocks inbound took xray down. The xray-valid aead_* aliases hit it too. The cipher is now read from either key, matched with the same table (and case-insensitivity) the core's own conf package uses, and an unrecognized one is an error instead of a guess. The legacy shadowsocks validator is also the only one that accepts a second user under an email it already holds, and RemoveUser then drops just one of them — a disabled or expired client kept connecting. AddUser now drops the email first on that account type so a single removal fully revokes the client. GetTraffic skipped every stat the first time it saw it. xray creates a counter on a user's first use, so that dropped a new client's traffic for a whole polling interval, as did the counter reset after a core restart. Only the first poll of a process is a baseline now; later, unseen and rewound counters both count from zero. Also fixes three unchecked settings["method"].(string) assertions that panic the panel on a shadowsocks inbound whose settings carry no method, and bounds TestRoute's port so an out-of-range value cannot wrap into the uint32 the core is asked about. Tests: api_users_e2e_test.go drives add/remove for every protocol against a real core and asserts it survives each one (skipped unless XRAY_E2E_BINARY is set); the account-type, traffic-delta and renew paths get unit coverage.
150 lines
5.5 KiB
Go
150 lines
5.5 KiB
Go
package xray
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/xtls/xray-core/proxy/shadowsocks"
|
|
"github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// decodeSSAccount decodes the typed message buildUserAccount produced for a
|
|
// shadowsocks user. The type URL is what the running inbound casts on, so the
|
|
// test asserts on it directly.
|
|
func decodeSSAccount(t *testing.T, user map[string]any) (typeURL string, legacy *shadowsocks.Account, modern *shadowsocks_2022.Account) {
|
|
t.Helper()
|
|
tm, err := buildUserAccount("shadowsocks", user)
|
|
if err != nil {
|
|
t.Fatalf("buildUserAccount: %v", err)
|
|
}
|
|
if tm == nil {
|
|
t.Fatal("buildUserAccount returned no account for shadowsocks")
|
|
}
|
|
typeURL = tm.Type
|
|
switch {
|
|
case strings.Contains(typeURL, "shadowsocks_2022"):
|
|
modern = new(shadowsocks_2022.Account)
|
|
if err := proto.Unmarshal(tm.Value, modern); err != nil {
|
|
t.Fatalf("unmarshal shadowsocks_2022 account: %v", err)
|
|
}
|
|
case strings.Contains(typeURL, "shadowsocks"):
|
|
legacy = new(shadowsocks.Account)
|
|
if err := proto.Unmarshal(tm.Value, legacy); err != nil {
|
|
t.Fatalf("unmarshal shadowsocks account: %v", err)
|
|
}
|
|
default:
|
|
t.Fatalf("unexpected account type %q", typeURL)
|
|
}
|
|
return typeURL, legacy, modern
|
|
}
|
|
|
|
func TestBuildUserAccountShadowsocksLegacyCiphers(t *testing.T) {
|
|
tests := []struct {
|
|
cipher string
|
|
want shadowsocks.CipherType
|
|
}{
|
|
{"aes-128-gcm", shadowsocks.CipherType_AES_128_GCM},
|
|
{"aead_aes_128_gcm", shadowsocks.CipherType_AES_128_GCM},
|
|
{"aes-256-gcm", shadowsocks.CipherType_AES_256_GCM},
|
|
{"AES-256-GCM", shadowsocks.CipherType_AES_256_GCM},
|
|
{"aead_aes_256_gcm", shadowsocks.CipherType_AES_256_GCM},
|
|
{"chacha20-poly1305", shadowsocks.CipherType_CHACHA20_POLY1305},
|
|
{"chacha20-ietf-poly1305", shadowsocks.CipherType_CHACHA20_POLY1305},
|
|
{"aead_chacha20_poly1305", shadowsocks.CipherType_CHACHA20_POLY1305},
|
|
{"xchacha20-poly1305", shadowsocks.CipherType_XCHACHA20_POLY1305},
|
|
{"xchacha20-ietf-poly1305", shadowsocks.CipherType_XCHACHA20_POLY1305},
|
|
{"aead_xchacha20_poly1305", shadowsocks.CipherType_XCHACHA20_POLY1305},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.cipher, func(t *testing.T) {
|
|
user := map[string]any{"email": "a@example.test", "password": "pw", "cipher": tt.cipher}
|
|
typeURL, legacy, modern := decodeSSAccount(t, user)
|
|
if modern != nil {
|
|
t.Fatalf("cipher %q built a shadowsocks-2022 account (%s); the legacy inbound casts to *shadowsocks.MemoryAccount and panics the core", tt.cipher, typeURL)
|
|
}
|
|
if legacy.CipherType != tt.want {
|
|
t.Fatalf("CipherType = %v, want %v", legacy.CipherType, tt.want)
|
|
}
|
|
if legacy.Password != "pw" {
|
|
t.Fatalf("Password = %q, want %q", legacy.Password, "pw")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildUserAccountShadowsocks2022Ciphers(t *testing.T) {
|
|
for _, cipher := range []string{
|
|
"2022-blake3-aes-128-gcm",
|
|
"2022-blake3-aes-256-gcm",
|
|
"2022-blake3-chacha20-poly1305",
|
|
} {
|
|
t.Run(cipher, func(t *testing.T) {
|
|
user := map[string]any{"email": "a@example.test", "password": b64Key(7), "cipher": cipher}
|
|
typeURL, _, modern := decodeSSAccount(t, user)
|
|
if modern == nil {
|
|
t.Fatalf("cipher %q built a legacy account (%s), want shadowsocks-2022", cipher, typeURL)
|
|
}
|
|
if modern.Key != b64Key(7) {
|
|
t.Fatalf("Key = %q, want the client password", modern.Key)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildUserAccountShadowsocksReadsMethodKey covers the client maps taken
|
|
// verbatim from an inbound's settings (the auto-renew path): they carry the
|
|
// inbound's cipher under "method", never "cipher".
|
|
func TestBuildUserAccountShadowsocksReadsMethodKey(t *testing.T) {
|
|
user := map[string]any{"email": "a@example.test", "password": "pw", "method": "aes-256-gcm"}
|
|
_, legacy, modern := decodeSSAccount(t, user)
|
|
if modern != nil {
|
|
t.Fatal("a client carrying only \"method\" must still build a legacy account")
|
|
}
|
|
if legacy.CipherType != shadowsocks.CipherType_AES_256_GCM {
|
|
t.Fatalf("CipherType = %v, want AES_256_GCM", legacy.CipherType)
|
|
}
|
|
}
|
|
|
|
func TestBuildUserAccountShadowsocksCipherKeyWins(t *testing.T) {
|
|
user := map[string]any{
|
|
"email": "a@example.test",
|
|
"password": b64Key(3),
|
|
"cipher": "2022-blake3-aes-128-gcm",
|
|
"method": "aes-256-gcm",
|
|
}
|
|
_, _, modern := decodeSSAccount(t, user)
|
|
if modern == nil {
|
|
t.Fatal("the explicit \"cipher\" must win over a stale \"method\"")
|
|
}
|
|
}
|
|
|
|
// TestBuildUserAccountShadowsocksUnknownCipherErrors is the regression for the
|
|
// account-type guess that killed the core: an unrecognized cipher used to fall
|
|
// through to a shadowsocks-2022 account, which xray's legacy inbound casts
|
|
// without checking, panicking the whole process.
|
|
func TestBuildUserAccountShadowsocksUnknownCipherErrors(t *testing.T) {
|
|
for _, cipher := range []string{"", "rc4-md5", "2022-blake3-future-gcm", "none"} {
|
|
t.Run("cipher="+cipher, func(t *testing.T) {
|
|
user := map[string]any{"email": "a@example.test", "password": "pw"}
|
|
if cipher != "" {
|
|
user["cipher"] = cipher
|
|
}
|
|
account, err := buildUserAccount("shadowsocks", user)
|
|
if err == nil {
|
|
t.Fatalf("cipher %q built account %v, want an error instead of an account-type guess", cipher, account)
|
|
}
|
|
if !strings.Contains(err.Error(), "unknown cipher") {
|
|
t.Fatalf("error = %q, want it to name the unknown cipher", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildUserAccountShadowsocksMissingPassword(t *testing.T) {
|
|
user := map[string]any{"email": "a@example.test", "cipher": "aes-256-gcm"}
|
|
if _, err := buildUserAccount("shadowsocks", user); err == nil {
|
|
t.Fatal("expected an error for a shadowsocks user without a password")
|
|
}
|
|
}
|