mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 00:24:19 +00:00
fix(inbound): regenerate SS-2022 client PSKs on method key-size change
Switching a Shadowsocks-2022 inbound between ciphers of different key sizes (e.g. aes-256 <-> aes-128) resized the server PSK but left existing client PSKs at the old length. xray rejects a wrong-length uPSK, so links stopped connecting. Regenerate mismatched client keys on inbound add/update, mirroring the single-client form's existing self-heal. Affected clients must re-subscribe.
This commit is contained in:
@@ -193,6 +193,49 @@ func shadowsocksKeyBytes(method string) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// normalizeShadowsocksClientKeys rewrites any Shadowsocks-2022 client password
|
||||
// whose decoded length no longer matches settings.method, which happens after the
|
||||
// inbound method is switched between ciphers of different key sizes (e.g.
|
||||
// aes-256↔aes-128). A wrong-length uPSK makes xray reject the user, so the link
|
||||
// fails to connect; regenerating restores a valid key (clients must re-fetch).
|
||||
// Non-Shadowsocks / legacy-SS settings pass through unchanged.
|
||||
func normalizeShadowsocksClientKeys(settings string) (string, bool) {
|
||||
method := shadowsocksMethodFromSettings(settings)
|
||||
if shadowsocksKeyBytes(method) == 0 {
|
||||
return settings, false
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal([]byte(settings), &m); err != nil {
|
||||
return settings, false
|
||||
}
|
||||
clients, ok := m["clients"].([]any)
|
||||
if !ok {
|
||||
return settings, false
|
||||
}
|
||||
changed := false
|
||||
for i := range clients {
|
||||
c, ok := clients[i].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if pw, _ := c["password"].(string); validShadowsocksClientKey(method, pw) {
|
||||
continue
|
||||
}
|
||||
c["password"] = randomShadowsocksClientKey(method)
|
||||
clients[i] = c
|
||||
changed = true
|
||||
}
|
||||
if !changed {
|
||||
return settings, false
|
||||
}
|
||||
m["clients"] = clients
|
||||
bs, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return settings, false
|
||||
}
|
||||
return string(bs), true
|
||||
}
|
||||
|
||||
func applyShadowsocksClientMethod(clients []any, settings map[string]any) {
|
||||
method, _ := settings["method"].(string)
|
||||
is2022 := strings.HasPrefix(method, "2022-blake3-")
|
||||
|
||||
@@ -619,6 +619,12 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||
}
|
||||
}
|
||||
|
||||
// Defensively fix any Shadowsocks-2022 client PSK whose length doesn't match
|
||||
// the inbound method (e.g. an API caller supplied a wrong-size key).
|
||||
if normalized, changed := normalizeShadowsocksClientKeys(inbound.Settings); changed {
|
||||
inbound.Settings = normalized
|
||||
}
|
||||
|
||||
// Secure client ID
|
||||
for _, client := range clients {
|
||||
switch inbound.Protocol {
|
||||
@@ -1041,6 +1047,14 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
}
|
||||
}
|
||||
|
||||
// A Shadowsocks-2022 method change resizes the key, but existing client PSKs
|
||||
// keep their old length and would be rejected by xray. Regenerate mismatched
|
||||
// client keys so the inbound stays connectable.
|
||||
if normalized, changed := normalizeShadowsocksClientKeys(inbound.Settings); changed {
|
||||
inbound.Settings = normalized
|
||||
logger.Warning("Shadowsocks inbound", inbound.Id, "method change resized keys; regenerated mismatched client PSK(s)")
|
||||
}
|
||||
|
||||
oldInbound.Total = inbound.Total
|
||||
oldInbound.Remark = inbound.Remark
|
||||
oldInbound.SubSortIndex = inbound.SubSortIndex
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A method switch between SS-2022 ciphers of different key sizes must regenerate
|
||||
// client PSKs whose length no longer matches; otherwise xray rejects the user.
|
||||
func TestNormalizeShadowsocksClientKeys_RegeneratesOnMethodResize(t *testing.T) {
|
||||
// 32-byte (aes-256-sized) client key under an aes-128 (16-byte) method.
|
||||
oversized := base64.StdEncoding.EncodeToString(make([]byte, 32))
|
||||
settings := `{"method":"2022-blake3-aes-128-gcm","password":"` +
|
||||
base64.StdEncoding.EncodeToString(make([]byte, 16)) +
|
||||
`","clients":[{"email":"a","password":"` + oversized + `"}]}`
|
||||
|
||||
out, changed := normalizeShadowsocksClientKeys(settings)
|
||||
if !changed {
|
||||
t.Fatalf("expected mismatched client key to be regenerated")
|
||||
}
|
||||
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal([]byte(out), &m); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
clients := m["clients"].([]any)
|
||||
pw := clients[0].(map[string]any)["password"].(string)
|
||||
if pw == oversized {
|
||||
t.Fatalf("client key was not regenerated")
|
||||
}
|
||||
if decoded, err := base64.StdEncoding.DecodeString(pw); err != nil || len(decoded) != 16 {
|
||||
t.Fatalf("regenerated key must be 16 bytes for aes-128, got len=%d err=%v", len(decoded), err)
|
||||
}
|
||||
}
|
||||
|
||||
// A correctly-sized key (and non-2022 / legacy settings) must pass through untouched.
|
||||
func TestNormalizeShadowsocksClientKeys_NoChangeWhenValid(t *testing.T) {
|
||||
valid := base64.StdEncoding.EncodeToString(make([]byte, 32))
|
||||
settings := `{"method":"2022-blake3-aes-256-gcm","clients":[{"email":"a","password":"` + valid + `"}]}`
|
||||
if out, changed := normalizeShadowsocksClientKeys(settings); changed || out != settings {
|
||||
t.Fatalf("valid aes-256 key must be left unchanged")
|
||||
}
|
||||
|
||||
legacy := `{"method":"aes-256-gcm","clients":[{"email":"a","password":"anything"}]}`
|
||||
if out, changed := normalizeShadowsocksClientKeys(legacy); changed || out != legacy {
|
||||
t.Fatalf("legacy (non-2022) SS settings must be left unchanged")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user