mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +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 = {
|
export const SSMethods = {
|
||||||
|
AES_256_GCM: 'aes-256-gcm',
|
||||||
CHACHA20_POLY1305: 'chacha20-poly1305',
|
CHACHA20_POLY1305: 'chacha20-poly1305',
|
||||||
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
|
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
|
||||||
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
|
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package random
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"math/big"
|
"math/big"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -59,3 +60,14 @@ func Num(n int) int {
|
||||||
}
|
}
|
||||||
return int(r.Int64())
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -14,6 +15,7 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/v3/logger"
|
"github.com/mhsanaei/3x-ui/v3/logger"
|
||||||
"github.com/mhsanaei/3x-ui/v3/util/common"
|
"github.com/mhsanaei/3x-ui/v3/util/common"
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/util/random"
|
||||||
"github.com/mhsanaei/3x-ui/v3/xray"
|
"github.com/mhsanaei/3x-ui/v3/xray"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
@ -360,7 +362,7 @@ func (s *ClientService) Create(inboundSvc *InboundService, payload *ClientCreate
|
||||||
if getErr != nil {
|
if getErr != nil {
|
||||||
return needRestart, getErr
|
return needRestart, getErr
|
||||||
}
|
}
|
||||||
if err := s.fillProtocolDefaults(&client, inbound.Protocol); err != nil {
|
if err := s.fillProtocolDefaults(&client, inbound); err != nil {
|
||||||
return needRestart, err
|
return needRestart, err
|
||||||
}
|
}
|
||||||
settingsPayload, mErr := json.Marshal(map[string][]model.Client{"clients": {client}})
|
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
|
return needRestart, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ClientService) fillProtocolDefaults(c *model.Client, p model.Protocol) error {
|
func (s *ClientService) fillProtocolDefaults(c *model.Client, ib *model.Inbound) error {
|
||||||
switch p {
|
switch ib.Protocol {
|
||||||
case model.VMESS, model.VLESS:
|
case model.VMESS, model.VLESS:
|
||||||
if c.ID == "" {
|
if c.ID == "" {
|
||||||
c.ID = uuid.NewString()
|
c.ID = uuid.NewString()
|
||||||
}
|
}
|
||||||
case model.Trojan, model.Shadowsocks:
|
case model.Trojan:
|
||||||
if c.Password == "" {
|
if c.Password == "" {
|
||||||
c.Password = strings.ReplaceAll(uuid.NewString(), "-", "")
|
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:
|
case model.Hysteria, model.Hysteria2:
|
||||||
if c.Auth == "" {
|
if c.Auth == "" {
|
||||||
c.Auth = strings.ReplaceAll(uuid.NewString(), "-", "")
|
c.Auth = strings.ReplaceAll(uuid.NewString(), "-", "")
|
||||||
|
|
@ -399,6 +406,80 @@ func (s *ClientService) fillProtocolDefaults(c *model.Client, p model.Protocol)
|
||||||
return nil
|
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) {
|
func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model.Client) (bool, error) {
|
||||||
existing, err := s.GetByID(id)
|
existing, err := s.GetByID(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -433,7 +514,7 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
|
||||||
if oldKey == "" {
|
if oldKey == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := s.fillProtocolDefaults(&updated, inbound.Protocol); err != nil {
|
if err := s.fillProtocolDefaults(&updated, inbound); err != nil {
|
||||||
return needRestart, err
|
return needRestart, err
|
||||||
}
|
}
|
||||||
settingsPayload, mErr := json.Marshal(map[string][]model.Client{"clients": {updated}})
|
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
|
return needRestart, getErr
|
||||||
}
|
}
|
||||||
copyClient := *clientWire
|
copyClient := *clientWire
|
||||||
if err := s.fillProtocolDefaults(©Client, inbound.Protocol); err != nil {
|
if err := s.fillProtocolDefaults(©Client, inbound); err != nil {
|
||||||
return needRestart, err
|
return needRestart, err
|
||||||
}
|
}
|
||||||
settingsPayload, mErr := json.Marshal(map[string][]model.Client{"clients": {copyClient}})
|
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
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if oldInbound.Protocol == model.Shadowsocks {
|
||||||
|
applyShadowsocksClientMethod(interfaceClients, oldSettings)
|
||||||
|
}
|
||||||
|
|
||||||
oldClients := oldSettings["clients"].([]any)
|
oldClients := oldSettings["clients"].([]any)
|
||||||
oldClients = append(oldClients, interfaceClients...)
|
oldClients = append(oldClients, interfaceClients...)
|
||||||
|
|
||||||
|
|
@ -1049,6 +1134,9 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
|
||||||
interfaceClients[0] = newMap
|
interfaceClients[0] = newMap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if oldInbound.Protocol == model.Shadowsocks {
|
||||||
|
applyShadowsocksClientMethod(interfaceClients, oldSettings)
|
||||||
|
}
|
||||||
settingsClients[clientIndex] = interfaceClients[0]
|
settingsClients[clientIndex] = interfaceClients[0]
|
||||||
oldSettings["clients"] = settingsClients
|
oldSettings["clients"] = settingsClients
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||||
|
|
@ -241,12 +242,62 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||||
inbound.StreamSettings = string(newStream)
|
inbound.StreamSettings = string(newStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if inbound.Protocol == model.Shadowsocks {
|
||||||
|
if healed, ok := healShadowsocksClientMethods(inbound.Settings); ok {
|
||||||
|
inbound.Settings = healed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inboundConfig := inbound.GenXrayInboundConfig()
|
inboundConfig := inbound.GenXrayInboundConfig()
|
||||||
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
|
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
|
||||||
}
|
}
|
||||||
return xrayConfig, nil
|
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.
|
// GetXrayTraffic fetches the current traffic statistics from the running Xray process.
|
||||||
func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
|
func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
|
||||||
if !s.IsXrayRunning() {
|
if !s.IsXrayRunning() {
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,8 @@ func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]an
|
||||||
|
|
||||||
var ssCipherType shadowsocks.CipherType
|
var ssCipherType shadowsocks.CipherType
|
||||||
switch cipher {
|
switch cipher {
|
||||||
|
case "aes-256-gcm":
|
||||||
|
ssCipherType = shadowsocks.CipherType_AES_256_GCM
|
||||||
case "chacha20-poly1305", "chacha20-ietf-poly1305":
|
case "chacha20-poly1305", "chacha20-ietf-poly1305":
|
||||||
ssCipherType = shadowsocks.CipherType_CHACHA20_POLY1305
|
ssCipherType = shadowsocks.CipherType_CHACHA20_POLY1305
|
||||||
case "xchacha20-poly1305", "xchacha20-ietf-poly1305":
|
case "xchacha20-poly1305", "xchacha20-ietf-poly1305":
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue