浏览代码

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.
MHSanaei 1 天之前
父节点
当前提交
982595968d

+ 43 - 0
internal/web/service/client_crud.go

@@ -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-")

+ 14 - 0
internal/web/service/inbound.go

@@ -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

+ 49 - 0
internal/web/service/shadowsocks_client_key_test.go

@@ -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")
+	}
+}