From 982595968dbb2fb08c383888b99383bc03662132 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 17 Jun 2026 14:11:35 +0200 Subject: [PATCH] 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. --- internal/web/service/client_crud.go | 43 ++++++++++++++++ internal/web/service/inbound.go | 14 ++++++ .../service/shadowsocks_client_key_test.go | 49 +++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 internal/web/service/shadowsocks_client_key_test.go diff --git a/internal/web/service/client_crud.go b/internal/web/service/client_crud.go index 565a81752..5f05ffde7 100644 --- a/internal/web/service/client_crud.go +++ b/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-") diff --git a/internal/web/service/inbound.go b/internal/web/service/inbound.go index 97357ac79..39cfd003f 100644 --- a/internal/web/service/inbound.go +++ b/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 diff --git a/internal/web/service/shadowsocks_client_key_test.go b/internal/web/service/shadowsocks_client_key_test.go new file mode 100644 index 000000000..c24fa433f --- /dev/null +++ b/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") + } +}