From ad8d58c2b6996d88a97f37176278feefcc354a46 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 27 May 2026 03:11:51 +0200 Subject: [PATCH] fix(xray): heal shadowsocks per-client method across all start paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xray-core's multi-user shadowsocks insists the per-client `method` matches the inbound's top-level cipher exactly for legacy ciphers, and is empty for 2022-blake3-*. The previous code (xray.go) copied `Client.Security` into the per-client `method` blindly, so a multi-protocol client created with the VMess default `"auto"` poisoned the SS config with `method: "auto"` → "unsupported cipher method: auto". Fix in two parts: - GetXrayConfig no longer projects `Client.Security` into the SS entry; the inbound's top-level method is now the single source of truth. - HealShadowsocksClientMethods moves to `database/model` and is invoked from `Inbound.GenXrayInboundConfig`, so the runtime add/update path (runtime.AddInbound) is normalised in addition to the full-restart path. For legacy ciphers heal now overwrites mismatched per-client methods rather than preserving them, so stale DB rows are also healed. --- database/model/model.go | 67 ++++++++++++++++++++++++++++++++++++++++- web/service/client.go | 26 +++++++++++----- web/service/xray.go | 48 +---------------------------- 3 files changed, 85 insertions(+), 56 deletions(-) diff --git a/database/model/model.go b/database/model/model.go index 642808ee..6c4230ad 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -219,17 +219,82 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { } listen = fmt.Sprintf("\"%v\"", listen) protocol := string(i.Protocol) + settings := i.Settings + if i.Protocol == Shadowsocks { + if healed, ok := HealShadowsocksClientMethods(settings); ok { + settings = healed + } + } return &xray.InboundConfig{ Listen: json_util.RawMessage(listen), Port: i.Port, Protocol: protocol, - Settings: json_util.RawMessage(i.Settings), + Settings: json_util.RawMessage(settings), StreamSettings: json_util.RawMessage(i.StreamSettings), Tag: i.Tag, Sniffing: json_util.RawMessage(i.Sniffing), } } +// HealShadowsocksClientMethods normalises the per-client `method` field +// on a shadowsocks inbound's settings JSON before it leaves for xray-core: +// - Legacy ciphers (aes-*, chacha20-*): every client must carry a +// per-user `method` matching the inbound's top-level method, otherwise +// xray fails with "unsupported cipher method:". +// - Shadowsocks 2022 (2022-blake3-*): xray's multi-user code rejects the +// inbound with "users must have empty method" when a client carries +// one — strip stale entries left over from a switch off a legacy +// cipher. +// Returns the rewritten settings string and true when anything changed. +func HealShadowsocksClientMethods(settings string) (string, bool) { + if settings == "" { + return settings, false + } + var parsed map[string]any + if err := json.Unmarshal([]byte(settings), &parsed); err != nil { + return settings, false + } + method, _ := parsed["method"].(string) + clients, ok := parsed["clients"].([]any) + if !ok { + return settings, false + } + is2022 := strings.HasPrefix(method, "2022-blake3-") + changed := false + for i := range clients { + cm, ok := clients[i].(map[string]any) + if !ok { + continue + } + if is2022 { + if _, hasKey := cm["method"]; hasKey { + delete(cm, "method") + clients[i] = cm + changed = true + } + continue + } + if method == "" { + continue + } + existing, _ := cm["method"].(string) + if existing == method { + continue + } + cm["method"] = method + clients[i] = cm + changed = true + } + if !changed { + return settings, false + } + out, err := json.MarshalIndent(parsed, "", " ") + if err != nil { + return settings, false + } + return string(out), true +} + // Setting stores key-value configuration settings for the 3x-ui panel. type Setting struct { Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` diff --git a/web/service/client.go b/web/service/client.go index 2b42b6ab..34831ae1 100644 --- a/web/service/client.go +++ b/web/service/client.go @@ -538,21 +538,31 @@ func shadowsocksKeyBytes(method string) int { return 0 } -// applyShadowsocksClientMethod ensures each client entry carries a "method" -// field for legacy shadowsocks ciphers. xray's multi-user shadowsocks code -// requires a per-client method; an empty/missing field fails with -// "unsupported cipher method:". 2022-blake3 ciphers use the top-level -// method only, so the per-client field must stay absent. +// applyShadowsocksClientMethod normalises the per-client "method" field +// when an inbound is created or updated: +// - Legacy ciphers: backfill `method` so xray's multi-user code is happy. +// "unsupported cipher method:" otherwise. +// - 2022-blake3-*: strip the per-client `method` because xray rejects +// it with "users must have empty method". This matters after an admin +// switches an existing inbound from a legacy cipher to a 2022 one. func applyShadowsocksClientMethod(clients []any, settings map[string]any) { method, _ := settings["method"].(string) - if method == "" || strings.HasPrefix(method, "2022-blake3-") { - return - } + is2022 := strings.HasPrefix(method, "2022-blake3-") for i := range clients { cm, ok := clients[i].(map[string]any) if !ok { continue } + if is2022 { + if _, hasKey := cm["method"]; hasKey { + delete(cm, "method") + clients[i] = cm + } + continue + } + if method == "" { + continue + } if existing, _ := cm["method"].(string); existing != "" { continue } diff --git a/web/service/xray.go b/web/service/xray.go index a9dd326c..0e9d6a4b 100644 --- a/web/service/xray.go +++ b/web/service/xray.go @@ -180,9 +180,6 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) { if c.Password != "" { entry["password"] = c.Password } - if c.Security != "" { - entry["method"] = c.Security - } case model.Hysteria: if c.Auth != "" { entry["auth"] = c.Auth @@ -246,7 +243,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) { } if inbound.Protocol == model.Shadowsocks { - if healed, ok := healShadowsocksClientMethods(inbound.Settings); ok { + if healed, ok := model.HealShadowsocksClientMethods(inbound.Settings); ok { inbound.Settings = healed } } @@ -307,49 +304,6 @@ func resolveXrayLogPaths(logCfg json_util.RawMessage) json_util.RawMessage { return out } -// healShadowsocksClientMethods is the same idea as applyShadowsocksClientMethod -// (see client.go) but applied at xray-config-build time, to backfill the -// per-client method field for legacy shadowsocks inbounds whose clients were -// stored before applyShadowsocksClientMethod existed. Returns the rewritten -// settings string and true when anything actually changed. -func healShadowsocksClientMethods(settings string) (string, bool) { - if settings == "" { - return settings, false - } - var parsed map[string]any - if err := json.Unmarshal([]byte(settings), &parsed); err != nil { - return settings, false - } - method, _ := parsed["method"].(string) - if method == "" || strings.HasPrefix(method, "2022-blake3-") { - return settings, false - } - clients, ok := parsed["clients"].([]any) - if !ok { - return settings, false - } - changed := false - for i := range clients { - cm, ok := clients[i].(map[string]any) - if !ok { - continue - } - if existing, _ := cm["method"].(string); existing != "" { - continue - } - cm["method"] = method - clients[i] = cm - changed = true - } - if !changed { - return settings, false - } - out, err := json.MarshalIndent(parsed, "", " ") - if err != nil { - return settings, false - } - return string(out), true -} // GetXrayTraffic fetches the current traffic statistics from the running Xray process. func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {