fix(xray): heal shadowsocks per-client method across all start paths

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.
This commit is contained in:
MHSanaei 2026-05-27 03:11:51 +02:00
parent 66f5026356
commit ad8d58c2b6
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
3 changed files with 85 additions and 56 deletions

View file

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

View file

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

View file

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