mirror of
https://github.com/XTLS/Xray-core.git
synced 2024-11-16 12:03:03 +00:00
Refactor: new Shadowsocks validator (#629)
* Refactor: new Shadowsocks validator * Fix NoneCliper cannot work * Feat: refine the size of drain * fix: fix validator after merge 'main' * fix: UDP user logic * style: refine code style
This commit is contained in:
parent
dd6769954c
commit
63d0cb1bd6
@ -7,8 +7,6 @@ import (
|
|||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/chacha20poly1305"
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
"golang.org/x/crypto/hkdf"
|
"golang.org/x/crypto/hkdf"
|
||||||
@ -28,6 +26,10 @@ type MemoryAccount struct {
|
|||||||
replayFilter antireplay.GeneralizedReplayFilter
|
replayFilter antireplay.GeneralizedReplayFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrIVNotUnique = newError("IV is not unique")
|
||||||
|
)
|
||||||
|
|
||||||
// Equals implements protocol.Account.Equals().
|
// Equals implements protocol.Account.Equals().
|
||||||
func (a *MemoryAccount) Equals(another protocol.Account) bool {
|
func (a *MemoryAccount) Equals(another protocol.Account) bool {
|
||||||
if account, ok := another.(*MemoryAccount); ok {
|
if account, ok := another.(*MemoryAccount); ok {
|
||||||
@ -43,24 +45,7 @@ func (a *MemoryAccount) CheckIV(iv []byte) error {
|
|||||||
if a.replayFilter.Check(iv) {
|
if a.replayFilter.Check(iv) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return newError("IV is not unique")
|
return ErrIVNotUnique
|
||||||
}
|
|
||||||
|
|
||||||
func (a *MemoryAccount) GetCipherName() string {
|
|
||||||
switch a.Cipher.(type) {
|
|
||||||
case *AEADCipher:
|
|
||||||
switch reflect.ValueOf(a.Cipher.(*AEADCipher).AEADAuthCreator).Pointer() {
|
|
||||||
case reflect.ValueOf(createAesGcm).Pointer():
|
|
||||||
keyBytes := a.Cipher.(*AEADCipher).KeyBytes
|
|
||||||
return "AES_" + strconv.FormatInt(int64(keyBytes*8), 10) + "_GCM"
|
|
||||||
case reflect.ValueOf(createChaCha20Poly1305).Pointer():
|
|
||||||
return "CHACHA20_POLY1305"
|
|
||||||
}
|
|
||||||
case *NoneCipher:
|
|
||||||
return "NONE"
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createAesGcm(key []byte) cipher.AEAD {
|
func createAesGcm(key []byte) cipher.AEAD {
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
package shadowsocks
|
package shadowsocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
|
||||||
"hash/crc32"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
@ -54,10 +50,7 @@ func (r *FullReader) Read(p []byte) (n int, err error) {
|
|||||||
|
|
||||||
// ReadTCPSession reads a Shadowsocks TCP session from the given reader, returns its header and remaining parts.
|
// ReadTCPSession reads a Shadowsocks TCP session from the given reader, returns its header and remaining parts.
|
||||||
func ReadTCPSession(validator *Validator, reader io.Reader) (*protocol.RequestHeader, buf.Reader, error) {
|
func ReadTCPSession(validator *Validator, reader io.Reader) (*protocol.RequestHeader, buf.Reader, error) {
|
||||||
hashkdf := hmac.New(sha256.New, []byte("SSBSKDF"))
|
behaviorSeed := validator.GetBehaviorSeed()
|
||||||
|
|
||||||
behaviorSeed := crc32.ChecksumIEEE(hashkdf.Sum(nil))
|
|
||||||
|
|
||||||
behaviorRand := dice.NewDeterministicDice(int64(behaviorSeed))
|
behaviorRand := dice.NewDeterministicDice(int64(behaviorSeed))
|
||||||
BaseDrainSize := behaviorRand.Roll(3266)
|
BaseDrainSize := behaviorRand.Roll(3266)
|
||||||
RandDrainMax := behaviorRand.Roll(64) + 1
|
RandDrainMax := behaviorRand.Roll(64) + 1
|
||||||
@ -65,23 +58,10 @@ func ReadTCPSession(validator *Validator, reader io.Reader) (*protocol.RequestHe
|
|||||||
DrainSize := BaseDrainSize + 16 + 38 + RandDrainRolled
|
DrainSize := BaseDrainSize + 16 + 38 + RandDrainRolled
|
||||||
readSizeRemain := DrainSize
|
readSizeRemain := DrainSize
|
||||||
|
|
||||||
var r2 buf.Reader
|
var r buf.Reader
|
||||||
buffer := buf.New()
|
buffer := buf.New()
|
||||||
defer buffer.Release()
|
defer buffer.Release()
|
||||||
|
|
||||||
var user *protocol.MemoryUser
|
|
||||||
var ivLen int32
|
|
||||||
var iv []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
count := validator.Count()
|
|
||||||
if count == 0 {
|
|
||||||
readSizeRemain -= int(buffer.Len())
|
|
||||||
DrainConnN(reader, readSizeRemain)
|
|
||||||
return nil, nil, newError("invalid user")
|
|
||||||
} else if count > 1 {
|
|
||||||
var aead cipher.AEAD
|
|
||||||
|
|
||||||
if _, err := buffer.ReadFullFrom(reader, 50); err != nil {
|
if _, err := buffer.ReadFullFrom(reader, 50); err != nil {
|
||||||
readSizeRemain -= int(buffer.Len())
|
readSizeRemain -= int(buffer.Len())
|
||||||
DrainConnN(reader, readSizeRemain)
|
DrainConnN(reader, readSizeRemain)
|
||||||
@ -89,48 +69,41 @@ func ReadTCPSession(validator *Validator, reader io.Reader) (*protocol.RequestHe
|
|||||||
}
|
}
|
||||||
|
|
||||||
bs := buffer.Bytes()
|
bs := buffer.Bytes()
|
||||||
user, aead, _, ivLen, err = validator.Get(bs, protocol.RequestCommandTCP)
|
user, aead, _, ivLen, err := validator.Get(bs, protocol.RequestCommandTCP)
|
||||||
|
|
||||||
if user != nil {
|
switch err {
|
||||||
if ivLen > 0 {
|
case ErrNotFound:
|
||||||
iv = append([]byte(nil), bs[:ivLen]...)
|
readSizeRemain -= int(buffer.Len())
|
||||||
}
|
DrainConnN(reader, readSizeRemain)
|
||||||
|
return nil, nil, newError("failed to match an user").Base(err)
|
||||||
|
case ErrIVNotUnique:
|
||||||
|
readSizeRemain -= int(buffer.Len())
|
||||||
|
DrainConnN(reader, readSizeRemain)
|
||||||
|
return nil, nil, newError("failed iv check").Base(err)
|
||||||
|
default:
|
||||||
reader = &FullReader{reader, bs[ivLen:]}
|
reader = &FullReader{reader, bs[ivLen:]}
|
||||||
|
readSizeRemain -= int(ivLen)
|
||||||
|
|
||||||
|
if aead != nil {
|
||||||
auth := &crypto.AEADAuthenticator{
|
auth := &crypto.AEADAuthenticator{
|
||||||
AEAD: aead,
|
AEAD: aead,
|
||||||
NonceGenerator: crypto.GenerateInitialAEADNonce(),
|
NonceGenerator: crypto.GenerateInitialAEADNonce(),
|
||||||
}
|
}
|
||||||
r2 = crypto.NewAuthenticationReader(auth, &crypto.AEADChunkSizeParser{
|
r = crypto.NewAuthenticationReader(auth, &crypto.AEADChunkSizeParser{
|
||||||
Auth: auth,
|
Auth: auth,
|
||||||
}, reader, protocol.TransferTypeStream, nil)
|
}, reader, protocol.TransferTypeStream, nil)
|
||||||
} else {
|
} else {
|
||||||
readSizeRemain -= int(buffer.Len())
|
|
||||||
DrainConnN(reader, readSizeRemain)
|
|
||||||
return nil, nil, newError("failed to match an user").Base(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
user, ivLen = validator.GetOnlyUser()
|
|
||||||
account := user.Account.(*MemoryAccount)
|
account := user.Account.(*MemoryAccount)
|
||||||
hashkdf.Write(account.Key)
|
iv := append([]byte(nil), buffer.BytesTo(ivLen)...)
|
||||||
if ivLen > 0 {
|
r, err = account.Cipher.NewDecryptionReader(account.Key, iv, reader)
|
||||||
if _, err := buffer.ReadFullFrom(reader, ivLen); err != nil {
|
|
||||||
readSizeRemain -= int(buffer.Len())
|
|
||||||
DrainConnN(reader, readSizeRemain)
|
|
||||||
return nil, nil, newError("failed to read IV").Base(err)
|
|
||||||
}
|
|
||||||
iv = append([]byte(nil), buffer.BytesTo(ivLen)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := account.Cipher.NewDecryptionReader(account.Key, iv, reader)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
readSizeRemain -= int(buffer.Len())
|
|
||||||
DrainConnN(reader, readSizeRemain)
|
DrainConnN(reader, readSizeRemain)
|
||||||
return nil, nil, newError("failed to initialize decoding stream").Base(err).AtError()
|
return nil, nil, newError("failed to initialize decoding stream").Base(err).AtError()
|
||||||
}
|
}
|
||||||
r2 = r
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
br := &buf.BufferedReader{Reader: r2}
|
br := &buf.BufferedReader{Reader: r}
|
||||||
|
|
||||||
request := &protocol.RequestHeader{
|
request := &protocol.RequestHeader{
|
||||||
Version: Version,
|
Version: Version,
|
||||||
@ -138,7 +111,6 @@ func ReadTCPSession(validator *Validator, reader io.Reader) (*protocol.RequestHe
|
|||||||
Command: protocol.RequestCommandTCP,
|
Command: protocol.RequestCommandTCP,
|
||||||
}
|
}
|
||||||
|
|
||||||
readSizeRemain -= int(buffer.Len())
|
|
||||||
buffer.Clear()
|
buffer.Clear()
|
||||||
|
|
||||||
addr, port, err := addrParser.ReadAddressPort(buffer, br)
|
addr, port, err := addrParser.ReadAddressPort(buffer, br)
|
||||||
@ -157,13 +129,6 @@ func ReadTCPSession(validator *Validator, reader io.Reader) (*protocol.RequestHe
|
|||||||
return nil, nil, newError("invalid remote address.")
|
return nil, nil, newError("invalid remote address.")
|
||||||
}
|
}
|
||||||
|
|
||||||
account := user.Account.(*MemoryAccount)
|
|
||||||
if ivError := account.CheckIV(iv); ivError != nil {
|
|
||||||
readSizeRemain -= int(buffer.Len())
|
|
||||||
DrainConnN(reader, readSizeRemain)
|
|
||||||
return nil, nil, newError("failed iv check").Base(ivError)
|
|
||||||
}
|
|
||||||
|
|
||||||
return request, br, nil
|
return request, br, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,36 +238,27 @@ func DecodeUDPPacket(validator *Validator, payload *buf.Buffer) (*protocol.Reque
|
|||||||
return nil, nil, newError("len(bs) <= 32")
|
return nil, nil, newError("len(bs) <= 32")
|
||||||
}
|
}
|
||||||
|
|
||||||
var user *protocol.MemoryUser
|
user, _, d, _, err := validator.Get(bs, protocol.RequestCommandUDP)
|
||||||
var err error
|
switch err {
|
||||||
|
case ErrIVNotUnique:
|
||||||
count := validator.Count()
|
return nil, nil, newError("failed iv check").Base(err)
|
||||||
if count == 0 {
|
case ErrNotFound:
|
||||||
return nil, nil, newError("invalid user")
|
return nil, nil, newError("failed to match an user").Base(err)
|
||||||
} else if count > 1 {
|
default:
|
||||||
var d []byte
|
account := user.Account.(*MemoryAccount)
|
||||||
user, _, d, _, err = validator.Get(bs, protocol.RequestCommandUDP)
|
if account.Cipher.IsAEAD() {
|
||||||
|
|
||||||
if user != nil {
|
|
||||||
payload.Clear()
|
payload.Clear()
|
||||||
payload.Write(d)
|
payload.Write(d)
|
||||||
} else {
|
} else {
|
||||||
return nil, nil, newError("failed to decrypt UDP payload").Base(err)
|
if account.Cipher.IVSize() > 0 {
|
||||||
}
|
iv := make([]byte, account.Cipher.IVSize())
|
||||||
} else {
|
|
||||||
user, _ = validator.GetOnlyUser()
|
|
||||||
account := user.Account.(*MemoryAccount)
|
|
||||||
|
|
||||||
var iv []byte
|
|
||||||
if !account.Cipher.IsAEAD() && account.Cipher.IVSize() > 0 {
|
|
||||||
// Keep track of IV as it gets removed from payload in DecodePacket.
|
|
||||||
iv = make([]byte, account.Cipher.IVSize())
|
|
||||||
copy(iv, payload.BytesTo(account.Cipher.IVSize()))
|
copy(iv, payload.BytesTo(account.Cipher.IVSize()))
|
||||||
}
|
}
|
||||||
if err = account.Cipher.DecodePacket(account.Key, payload); err != nil {
|
if err = account.Cipher.DecodePacket(account.Key, payload); err != nil {
|
||||||
return nil, nil, newError("failed to decrypt UDP payload").Base(err)
|
return nil, nil, newError("failed to decrypt UDP payload").Base(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
request := &protocol.RequestHeader{
|
request := &protocol.RequestHeader{
|
||||||
Version: Version,
|
Version: Version,
|
||||||
|
@ -115,10 +115,6 @@ func (s *Server) handleUDPPayload(ctx context.Context, conn stat.Connection, dis
|
|||||||
panic("no inbound metadata")
|
panic("no inbound metadata")
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.validator.Count() == 1 {
|
|
||||||
inbound.User, _ = s.validator.GetOnlyUser()
|
|
||||||
}
|
|
||||||
|
|
||||||
var dest *net.Destination
|
var dest *net.Destination
|
||||||
|
|
||||||
reader := buf.NewPacketReader(conn)
|
reader := buf.NewPacketReader(conn)
|
||||||
|
@ -2,112 +2,127 @@ package shadowsocks
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"hash/crc64"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/dice"
|
||||||
"github.com/xtls/xray-core/common/protocol"
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Validator stores valid Shadowsocks users.
|
// Validator stores valid Shadowsocks users.
|
||||||
type Validator struct {
|
type Validator struct {
|
||||||
// Considering email's usage here, map + sync.Mutex/RWMutex may have better performance.
|
sync.RWMutex
|
||||||
email sync.Map
|
users []*protocol.MemoryUser
|
||||||
users sync.Map
|
|
||||||
|
behaviorSeed uint64
|
||||||
|
behaviorFused bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a Shadowsocks user, Email must be empty or unique.
|
var (
|
||||||
|
ErrNotFound = newError("Not Found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add a Shadowsocks user.
|
||||||
func (v *Validator) Add(u *protocol.MemoryUser) error {
|
func (v *Validator) Add(u *protocol.MemoryUser) error {
|
||||||
|
v.Lock()
|
||||||
|
defer v.Unlock()
|
||||||
|
|
||||||
account := u.Account.(*MemoryAccount)
|
account := u.Account.(*MemoryAccount)
|
||||||
|
if !account.Cipher.IsAEAD() && len(v.users) > 0 {
|
||||||
|
return newError("The cipher is not support Single-port Multi-user")
|
||||||
|
}
|
||||||
|
v.users = append(v.users, u)
|
||||||
|
|
||||||
if !account.Cipher.IsAEAD() && v.Count() > 0 {
|
if !v.behaviorFused {
|
||||||
return newError("The cipher do not support Single-port Multi-user")
|
hashkdf := hmac.New(sha256.New, []byte("SSBSKDF"))
|
||||||
|
hashkdf.Write(account.Key)
|
||||||
|
v.behaviorSeed = crc64.Update(v.behaviorSeed, crc64.MakeTable(crc64.ECMA), hashkdf.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
if u.Email != "" {
|
|
||||||
_, loaded := v.email.LoadOrStore(strings.ToLower(u.Email), u)
|
|
||||||
if loaded {
|
|
||||||
return newError("User ", u.Email, " already exists.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v.users.Store(string(account.Key)+"&"+account.GetCipherName(), u)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Del a Shadowsocks user with a non-empty Email.
|
// Del a Shadowsocks user with a non-empty Email.
|
||||||
func (v *Validator) Del(e string) error {
|
func (v *Validator) Del(email string) error {
|
||||||
if e == "" {
|
if email == "" {
|
||||||
return newError("Email must not be empty.")
|
return newError("Email must not be empty.")
|
||||||
}
|
}
|
||||||
le := strings.ToLower(e)
|
|
||||||
u, _ := v.email.Load(le)
|
v.Lock()
|
||||||
if u == nil {
|
defer v.Unlock()
|
||||||
return newError("User ", e, " not found.")
|
|
||||||
|
email = strings.ToLower(email)
|
||||||
|
idx := -1
|
||||||
|
for i, u := range v.users {
|
||||||
|
if strings.EqualFold(u.Email, email) {
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
}
|
}
|
||||||
account := u.(*protocol.MemoryUser).Account.(*MemoryAccount)
|
}
|
||||||
v.email.Delete(le)
|
|
||||||
v.users.Delete(string(account.Key) + "&" + account.GetCipherName())
|
if idx == -1 {
|
||||||
|
return newError("User ", email, " not found.")
|
||||||
|
}
|
||||||
|
ulen := len(v.users)
|
||||||
|
|
||||||
|
v.users[idx] = v.users[ulen-1]
|
||||||
|
v.users[ulen-1] = nil
|
||||||
|
v.users = v.users[:ulen-1]
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count the number of Shadowsocks users
|
// Get a Shadowsocks user.
|
||||||
func (v *Validator) Count() int {
|
|
||||||
length := 0
|
|
||||||
v.users.Range(func(_, _ interface{}) bool {
|
|
||||||
length++
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return length
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a Shadowsocks user and the user's cipher.
|
|
||||||
func (v *Validator) Get(bs []byte, command protocol.RequestCommand) (u *protocol.MemoryUser, aead cipher.AEAD, ret []byte, ivLen int32, err error) {
|
func (v *Validator) Get(bs []byte, command protocol.RequestCommand) (u *protocol.MemoryUser, aead cipher.AEAD, ret []byte, ivLen int32, err error) {
|
||||||
var dataSize int
|
v.RLock()
|
||||||
|
defer v.RUnlock()
|
||||||
|
|
||||||
switch command {
|
for _, user := range v.users {
|
||||||
case protocol.RequestCommandTCP:
|
if account := user.Account.(*MemoryAccount); account.Cipher.IsAEAD() {
|
||||||
dataSize = 16
|
aeadCipher := account.Cipher.(*AEADCipher)
|
||||||
case protocol.RequestCommandUDP:
|
|
||||||
dataSize = 8192
|
|
||||||
}
|
|
||||||
|
|
||||||
var aeadCipher *AEADCipher
|
|
||||||
subkey := make([]byte, 32)
|
|
||||||
data := make([]byte, dataSize)
|
|
||||||
|
|
||||||
v.users.Range(func(key, user interface{}) bool {
|
|
||||||
account := user.(*protocol.MemoryUser).Account.(*MemoryAccount)
|
|
||||||
aeadCipher = account.Cipher.(*AEADCipher)
|
|
||||||
ivLen = aeadCipher.IVSize()
|
ivLen = aeadCipher.IVSize()
|
||||||
|
iv := bs[:ivLen]
|
||||||
|
subkey := make([]byte, 32)
|
||||||
subkey = subkey[:aeadCipher.KeyBytes]
|
subkey = subkey[:aeadCipher.KeyBytes]
|
||||||
hkdfSHA1(account.Key, bs[:ivLen], subkey)
|
hkdfSHA1(account.Key, iv, subkey)
|
||||||
aead = aeadCipher.AEADAuthCreator(subkey)
|
aead = aeadCipher.AEADAuthCreator(subkey)
|
||||||
|
|
||||||
|
var matchErr error
|
||||||
switch command {
|
switch command {
|
||||||
case protocol.RequestCommandTCP:
|
case protocol.RequestCommandTCP:
|
||||||
ret, err = aead.Open(data[:0], data[4:16], bs[ivLen:ivLen+18], nil)
|
data := make([]byte, 16)
|
||||||
|
ret, matchErr = aead.Open(data[:0], data[4:16], bs[ivLen:ivLen+18], nil)
|
||||||
case protocol.RequestCommandUDP:
|
case protocol.RequestCommandUDP:
|
||||||
ret, err = aead.Open(data[:0], data[8180:8192], bs[ivLen:], nil)
|
data := make([]byte, 8192)
|
||||||
|
ret, matchErr = aead.Open(data[:0], data[8180:8192], bs[ivLen:], nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if matchErr == nil {
|
||||||
u = user.(*protocol.MemoryUser)
|
u = user
|
||||||
return false
|
err = account.CheckIV(iv)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return true
|
} else {
|
||||||
})
|
u = user
|
||||||
|
ivLen = user.Account.(*MemoryAccount).Cipher.IVSize()
|
||||||
|
// err = user.Account.(*MemoryAccount).CheckIV(bs[:ivLen]) // The IV size of None Cipher is 0.
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, nil, 0, ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the only user without authentication
|
func (v *Validator) GetBehaviorSeed() uint64 {
|
||||||
func (v *Validator) GetOnlyUser() (u *protocol.MemoryUser, ivLen int32) {
|
v.Lock()
|
||||||
v.users.Range(func(_, user interface{}) bool {
|
defer v.Unlock()
|
||||||
u = user.(*protocol.MemoryUser)
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
ivLen = u.Account.(*MemoryAccount).Cipher.IVSize()
|
|
||||||
|
|
||||||
return
|
v.behaviorFused = true
|
||||||
|
if v.behaviorSeed == 0 {
|
||||||
|
v.behaviorSeed = dice.RollUint64()
|
||||||
|
}
|
||||||
|
return v.behaviorSeed
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user