修复SS2022的错误
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user