mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
fix(shadowsocks): keep a 2022 inbound with no enabled client locked
When every client of a Shadowsocks-2022 inbound was disabled the panel handed the core an empty client list, from which xray-core builds a single-user server keyed by the inbound's server key alone. That key is the first half of every link the panel ever issued, so a client cut off for quota, expiry or by the operator connected again with it. An inbound left without clients now gets a placeholder user whose key is random and held only in memory, in the config and in the running core alike. The core stays multi-user, and disabling the last real client takes the same remove-and-restart path as any other client, so its live sessions end as well.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
package xray
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/proxy/shadowsocks"
|
||||
"github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -147,3 +149,45 @@ func TestBuildUserAccountShadowsocksMissingPassword(t *testing.T) {
|
||||
t.Fatal("expected an error for a shadowsocks user without a password")
|
||||
}
|
||||
}
|
||||
|
||||
// With no client the core builds a single-user Shadowsocks-2022 server keyed by the server
|
||||
// key alone, which is the first half of every link handed out; the lock must prevent that.
|
||||
func TestLockShadowsocks2022KeepsTheCoreMultiUser(t *testing.T) {
|
||||
const key = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY="
|
||||
const user = `[{"email":"a@x","password":"` + key + `"}]`
|
||||
for name, tc := range map[string]struct {
|
||||
settings string
|
||||
locks bool
|
||||
}{
|
||||
"empty clients": {`{"method":"2022-blake3-aes-256-gcm","password":"` + key + `","clients":[]}`, true},
|
||||
"no clients key": {`{"method":"2022-blake3-aes-256-gcm","password":"` + key + `"}`, true},
|
||||
"upper-case method": {`{"method":"2022-BLAKE3-AES-256-GCM","password":"` + key + `","clients":[]}`, true},
|
||||
"aes-128": {`{"method":"2022-blake3-aes-128-gcm","password":"MDEyMzQ1Njc4OWFiY2RlZg==","clients":[]}`, true},
|
||||
"empty clients hide users": {`{"method":"2022-blake3-aes-256-gcm","password":"` + key + `","clients":[],"users":` + user + `}`, true},
|
||||
"one client": {`{"method":"2022-blake3-aes-256-gcm","password":"` + key + `","clients":` + user + `}`, false},
|
||||
"users only": {`{"method":"2022-blake3-aes-256-gcm","password":"` + key + `","users":` + user + `}`, false},
|
||||
"legacy method": {`{"method":"aes-256-gcm","password":"x","clients":[]}`, false},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var settings map[string]any
|
||||
if err := json.Unmarshal([]byte(tc.settings), &settings); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if got := LockShadowsocks2022(settings); got != tc.locks {
|
||||
t.Fatalf("LockShadowsocks2022 = %v, want %v", got, tc.locks)
|
||||
}
|
||||
raw, _ := json.Marshal(settings)
|
||||
var parsed conf.ShadowsocksServerConfig
|
||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||
t.Fatalf("unmarshal locked settings: %v", err)
|
||||
}
|
||||
built, err := parsed.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("core refuses the locked settings: %v", err)
|
||||
}
|
||||
if _, singleUser := built.(*shadowsocks_2022.ServerConfig); singleUser {
|
||||
t.Fatalf("core serves %s to the server key alone", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
package xray
|
||||
|
||||
import "github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"sync"
|
||||
|
||||
"github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||
)
|
||||
|
||||
// Shadowsocks2022KeyBytes is the key size a Shadowsocks-2022 method takes, or 0
|
||||
// for any other method, whose password is free-form.
|
||||
@@ -25,3 +31,36 @@ func Shadowsocks2022NoMultiUser(method string) bool {
|
||||
cipher, err := shadowsocks_2022.GetCipherMethod(method)
|
||||
return err == nil && cipher.IsChaCha
|
||||
}
|
||||
|
||||
// LockedShadowsocks2022Email names the placeholder user; the panel refuses "/" in a
|
||||
// client email, so it never collides with a real one.
|
||||
const LockedShadowsocks2022Email = "/locked"
|
||||
|
||||
var lockedShadowsocks2022Key = sync.OnceValue(func() []byte {
|
||||
key := make([]byte, 32)
|
||||
_, _ = rand.Read(key)
|
||||
return key
|
||||
})
|
||||
|
||||
// LockShadowsocks2022 gives a Shadowsocks-2022 inbound left with no client a user whose key
|
||||
// no link carries: with none, the core serves the inbound to its server key alone.
|
||||
func LockShadowsocks2022(settings map[string]any) bool {
|
||||
method, _ := settings["method"].(string)
|
||||
n := Shadowsocks2022KeyBytes(method)
|
||||
if n == 0 {
|
||||
return false
|
||||
}
|
||||
// The core reads clients over users, so users count only when clients is absent.
|
||||
users, hasClients := settings["clients"]
|
||||
if !hasClients {
|
||||
users = settings["users"]
|
||||
}
|
||||
if list, _ := users.([]any); len(list) > 0 {
|
||||
return false
|
||||
}
|
||||
settings["clients"] = []any{map[string]any{
|
||||
"email": LockedShadowsocks2022Email,
|
||||
"password": base64.StdEncoding.EncodeToString(lockedShadowsocks2022Key()[:n]),
|
||||
}}
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user