mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 13:14:11 +00:00
fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1d299ac396
commit
ef98a932d7
5 changed files with 160 additions and 6 deletions
|
|
@ -17,6 +17,7 @@ export const Protocols = {
|
|||
};
|
||||
|
||||
export const SSMethods = {
|
||||
AES_256_GCM: 'aes-256-gcm',
|
||||
CHACHA20_POLY1305: 'chacha20-poly1305',
|
||||
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
|
||||
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package random
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
|
|
@ -59,3 +60,14 @@ func Num(n int) int {
|
|||
}
|
||||
return int(r.Int64())
|
||||
}
|
||||
|
||||
// Base64Bytes returns n cryptographically-random bytes encoded as standard
|
||||
// base64 (with padding). Used for ss2022 keys, which xray expects as a
|
||||
// base64-encoded key of a specific byte length per cipher.
|
||||
func Base64Bytes(n int) string {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic("crypto/rand failed: " + err.Error())
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package service
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -14,6 +15,7 @@ import (
|
|||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/logger"
|
||||
"github.com/mhsanaei/3x-ui/v3/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v3/util/random"
|
||||
"github.com/mhsanaei/3x-ui/v3/xray"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
|
@ -360,7 +362,7 @@ func (s *ClientService) Create(inboundSvc *InboundService, payload *ClientCreate
|
|||
if getErr != nil {
|
||||
return needRestart, getErr
|
||||
}
|
||||
if err := s.fillProtocolDefaults(&client, inbound.Protocol); err != nil {
|
||||
if err := s.fillProtocolDefaults(&client, inbound); err != nil {
|
||||
return needRestart, err
|
||||
}
|
||||
settingsPayload, mErr := json.Marshal(map[string][]model.Client{"clients": {client}})
|
||||
|
|
@ -381,16 +383,21 @@ func (s *ClientService) Create(inboundSvc *InboundService, payload *ClientCreate
|
|||
return needRestart, nil
|
||||
}
|
||||
|
||||
func (s *ClientService) fillProtocolDefaults(c *model.Client, p model.Protocol) error {
|
||||
switch p {
|
||||
func (s *ClientService) fillProtocolDefaults(c *model.Client, ib *model.Inbound) error {
|
||||
switch ib.Protocol {
|
||||
case model.VMESS, model.VLESS:
|
||||
if c.ID == "" {
|
||||
c.ID = uuid.NewString()
|
||||
}
|
||||
case model.Trojan, model.Shadowsocks:
|
||||
case model.Trojan:
|
||||
if c.Password == "" {
|
||||
c.Password = strings.ReplaceAll(uuid.NewString(), "-", "")
|
||||
}
|
||||
case model.Shadowsocks:
|
||||
method := shadowsocksMethodFromSettings(ib.Settings)
|
||||
if c.Password == "" || !validShadowsocksClientKey(method, c.Password) {
|
||||
c.Password = randomShadowsocksClientKey(method)
|
||||
}
|
||||
case model.Hysteria, model.Hysteria2:
|
||||
if c.Auth == "" {
|
||||
c.Auth = strings.ReplaceAll(uuid.NewString(), "-", "")
|
||||
|
|
@ -399,6 +406,80 @@ func (s *ClientService) fillProtocolDefaults(c *model.Client, p model.Protocol)
|
|||
return nil
|
||||
}
|
||||
|
||||
// shadowsocksMethodFromSettings pulls the "method" field out of the inbound's
|
||||
// settings JSON. Returns "" when the field is missing or settings is invalid.
|
||||
func shadowsocksMethodFromSettings(settings string) string {
|
||||
if settings == "" {
|
||||
return ""
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal([]byte(settings), &m); err != nil {
|
||||
return ""
|
||||
}
|
||||
method, _ := m["method"].(string)
|
||||
return method
|
||||
}
|
||||
|
||||
// randomShadowsocksClientKey returns a per-client key sized to the cipher.
|
||||
// The 2022-blake3 ciphers require a base64-encoded key of an exact byte
|
||||
// length (16 bytes for aes-128-gcm, 32 bytes for aes-256-gcm and
|
||||
// chacha20-poly1305) — anything else fails with "bad key" on xray start.
|
||||
// Older ciphers accept arbitrary passwords, so we keep the uuid-style.
|
||||
func randomShadowsocksClientKey(method string) string {
|
||||
if n := shadowsocksKeyBytes(method); n > 0 {
|
||||
return random.Base64Bytes(n)
|
||||
}
|
||||
return strings.ReplaceAll(uuid.NewString(), "-", "")
|
||||
}
|
||||
|
||||
// validShadowsocksClientKey reports whether key is acceptable for the cipher.
|
||||
// For 2022-blake3 it must decode to the exact byte length the cipher needs;
|
||||
// any other method accepts any non-empty string.
|
||||
func validShadowsocksClientKey(method, key string) bool {
|
||||
n := shadowsocksKeyBytes(method)
|
||||
if n == 0 {
|
||||
return key != ""
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return len(decoded) == n
|
||||
}
|
||||
|
||||
func shadowsocksKeyBytes(method string) int {
|
||||
switch method {
|
||||
case "2022-blake3-aes-128-gcm":
|
||||
return 16
|
||||
case "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305":
|
||||
return 32
|
||||
}
|
||||
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.
|
||||
func applyShadowsocksClientMethod(clients []any, settings map[string]any) {
|
||||
method, _ := settings["method"].(string)
|
||||
if method == "" || strings.HasPrefix(method, "2022-blake3-") {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model.Client) (bool, error) {
|
||||
existing, err := s.GetByID(id)
|
||||
if err != nil {
|
||||
|
|
@ -433,7 +514,7 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
|
|||
if oldKey == "" {
|
||||
continue
|
||||
}
|
||||
if err := s.fillProtocolDefaults(&updated, inbound.Protocol); err != nil {
|
||||
if err := s.fillProtocolDefaults(&updated, inbound); err != nil {
|
||||
return needRestart, err
|
||||
}
|
||||
settingsPayload, mErr := json.Marshal(map[string][]model.Client{"clients": {updated}})
|
||||
|
|
@ -530,7 +611,7 @@ func (s *ClientService) Attach(inboundSvc *InboundService, id int, inboundIds []
|
|||
return needRestart, getErr
|
||||
}
|
||||
copyClient := *clientWire
|
||||
if err := s.fillProtocolDefaults(©Client, inbound.Protocol); err != nil {
|
||||
if err := s.fillProtocolDefaults(©Client, inbound); err != nil {
|
||||
return needRestart, err
|
||||
}
|
||||
settingsPayload, mErr := json.Marshal(map[string][]model.Client{"clients": {copyClient}})
|
||||
|
|
@ -871,6 +952,10 @@ func (s *ClientService) AddInboundClient(inboundSvc *InboundService, data *model
|
|||
return false, err
|
||||
}
|
||||
|
||||
if oldInbound.Protocol == model.Shadowsocks {
|
||||
applyShadowsocksClientMethod(interfaceClients, oldSettings)
|
||||
}
|
||||
|
||||
oldClients := oldSettings["clients"].([]any)
|
||||
oldClients = append(oldClients, interfaceClients...)
|
||||
|
||||
|
|
@ -1049,6 +1134,9 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
|
|||
interfaceClients[0] = newMap
|
||||
}
|
||||
}
|
||||
if oldInbound.Protocol == model.Shadowsocks {
|
||||
applyShadowsocksClientMethod(interfaceClients, oldSettings)
|
||||
}
|
||||
settingsClients[clientIndex] = interfaceClients[0]
|
||||
oldSettings["clients"] = settingsClients
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
|
|
@ -241,12 +242,62 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
|||
inbound.StreamSettings = string(newStream)
|
||||
}
|
||||
|
||||
if inbound.Protocol == model.Shadowsocks {
|
||||
if healed, ok := healShadowsocksClientMethods(inbound.Settings); ok {
|
||||
inbound.Settings = healed
|
||||
}
|
||||
}
|
||||
|
||||
inboundConfig := inbound.GenXrayInboundConfig()
|
||||
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
|
||||
}
|
||||
return xrayConfig, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if !s.IsXrayRunning() {
|
||||
|
|
|
|||
|
|
@ -212,6 +212,8 @@ func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]an
|
|||
|
||||
var ssCipherType shadowsocks.CipherType
|
||||
switch cipher {
|
||||
case "aes-256-gcm":
|
||||
ssCipherType = shadowsocks.CipherType_AES_256_GCM
|
||||
case "chacha20-poly1305", "chacha20-ietf-poly1305":
|
||||
ssCipherType = shadowsocks.CipherType_CHACHA20_POLY1305
|
||||
case "xchacha20-poly1305", "xchacha20-ietf-poly1305":
|
||||
|
|
|
|||
Loading…
Reference in a new issue