1
0
Эх сурвалжийг харах

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.
MHSanaei 14 цаг өмнө
parent
commit
ad8d58c2b6

+ 66 - 1
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"`

+ 18 - 8
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
 		}

+ 1 - 47
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) {