修复SS2022的错误

This commit is contained in:
CN-JS-HuiBai
2026-04-15 01:12:54 +08:00
parent be0f2fc891
commit 463338115f

View File

@@ -3,7 +3,7 @@ package xboard
import (
"bytes"
"context"
"crypto/md5"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
@@ -12,6 +12,7 @@ import (
"net/netip"
"net/url"
"strconv"
"strings"
"sync"
"time"
@@ -26,26 +27,28 @@ import (
"github.com/sagernet/sing/service"
)
func fixSSKey(key string, length int) string {
if key == "" {
return ""
// ss2022UserKey derives a per-user key for SS2022 exactly like V2bX:
// Take the first `keyLen` bytes of the UUID string, then base64 encode.
func ss2022UserKey(uuid string, keyLen int) string {
if len(uuid) < keyLen {
// Pad with zeros if UUID is shorter than required (shouldn't happen with standard UUIDs)
padded := make([]byte, keyLen)
copy(padded, []byte(uuid))
return base64.StdEncoding.EncodeToString(padded)
}
// If it's already a valid Base64 of the correct length, use it directly (as a B64 key)
if data, err := base64.StdEncoding.DecodeString(key); err == nil && len(data) == length {
return key
return base64.StdEncoding.EncodeToString([]byte(uuid[:keyLen]))
}
// ss2022KeyLength returns the required key length for a given SS2022 cipher.
func ss2022KeyLength(cipher string) int {
switch cipher {
case "2022-blake3-aes-128-gcm":
return 16
case "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305":
return 32
default:
return 32
}
// Xboard style for Shadowsocks 2022: Base64(MD5_Hex(password))
// 32 hex characters happen to be exactly 32 bytes of ASCII, perfect for aes-256-gcm
hash := md5.Sum([]byte(key))
hexHash := fmt.Sprintf("%x", hash)
// For 128-bit methods, truncate the hex string to 16
if length == 16 && len(hexHash) > 16 {
hexHash = hexHash[:16]
}
return base64.StdEncoding.EncodeToString([]byte(hexHash))
}
func RegisterService(registry *boxService.Registry) {
@@ -69,6 +72,7 @@ type Service struct {
aliveTicker *time.Ticker
access sync.Mutex
inboundManager adapter.InboundManager
ssCipher string // stored for user key derivation in syncUsers
}
type XBoardServiceOptions struct {
@@ -601,26 +605,35 @@ func (s *Service) setupNode() error {
if method == "" {
method = "aes-256-gcm"
}
// V2bX SS2022 key handling
if len(method) >= 4 && method[:4] == "2022" {
keyLen := 32
if len(method) >= 18 && method[13:16] == "128" {
keyLen = 16
}
serverKey = fixSSKey(serverKey, keyLen)
}
// Store cipher for user key derivation in syncUsers
s.ssCipher = method
// V2bX approach: use server_key from panel DIRECTLY as PSK
// The panel provides it already in the correct format (base64 for 2022)
ssOptions := &option.ShadowsocksInboundOptions{
ListenOptions: listen,
Method: method,
Password: serverKey,
}
inboundOptions = ssOptions
if len(serverKey) >= 8 {
s.logger.Info("Xboard Shadowsocks setup. Method: ", method, " PSK preview: ", serverKey[:8], "...")
if strings.Contains(method, "2022") {
// SS2022: server_key is the PSK, users get per-user keys
ssOptions.Password = serverKey
// Create a dummy user (will be replaced by syncUsers)
keyLen := ss2022KeyLength(method)
dummyKey := make([]byte, keyLen)
_, _ = rand.Read(dummyKey)
ssOptions.Users = []option.ShadowsocksUser{{
Password: base64.StdEncoding.EncodeToString(dummyKey),
}}
s.logger.Info("Xboard SS2022 setup. Method: ", method, " ServerKey (PSK) used directly from panel")
} else {
// Legacy SS: password-based
ssOptions.Password = serverKey
s.logger.Info("Xboard Shadowsocks setup. Method: ", method)
}
inboundOptions = ssOptions
case "trojan":
// Trojan supports ws/grpc transport like V2bX
transport, err := getInboundTransport(networkType, networkSettings)
@@ -864,9 +877,10 @@ func (s *Service) syncUsers() {
defer s.access.Unlock()
newUsers := make(map[string]userData)
isSS2022 := false
if s.options.NodeType == "shadowsocks" {
isSS2022 = true
isSS2022 := strings.Contains(s.ssCipher, "2022")
ss2022KeyLen := 0
if isSS2022 {
ss2022KeyLen = ss2022KeyLength(s.ssCipher)
}
for _, u := range users {
@@ -874,10 +888,11 @@ func (s *Service) syncUsers() {
if key == "" {
continue
}
// CRITICAL: Apply MD5-Hex hardening to each USER key for SS 2022
// V2bX approach for SS2022 user key:
// Take first keyLen bytes of UUID, then base64 encode
if isSS2022 {
key = fixSSKey(key, 32)
key = ss2022UserKey(key, ss2022KeyLen)
}
newUsers[u.Email] = userData{
@@ -906,21 +921,7 @@ func (s *Service) syncUsers() {
// Update local ID mapping
s.localUsers = newUsers
// Compatibility Hack: For SS 2022 without colon,
// we may need the first user's key as the global PSK
if isSS2022 && len(newUsers) > 0 {
for _, u := range newUsers {
for _, managedServer := range s.servers {
if ssInbound, ok := managedServer.(interface{ SetPassword(string) }); ok {
ssInbound.SetPassword(u.Key)
s.logger.Info("Compatibility Mode: Set global PSK to user key: ", u.Key[:8], "...")
}
}
break // Only use the first one as fallback
}
}
s.logger.Info("Xboard sync completed, total users: ", len(users))
}