mirror of
https://github.com/XTLS/Xray-core.git
synced 2025-04-29 16:58:34 +00:00
v1.0.0
This commit is contained in:
parent
47d23e9972
commit
c7f7c08ead
711 changed files with 82154 additions and 2 deletions
119
proxy/vmess/encoding/auth.go
Normal file
119
proxy/vmess/encoding/auth.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package encoding
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
"hash/fnv"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
// Authenticate authenticates a byte array using Fnv hash.
|
||||
func Authenticate(b []byte) uint32 {
|
||||
fnv1hash := fnv.New32a()
|
||||
common.Must2(fnv1hash.Write(b))
|
||||
return fnv1hash.Sum32()
|
||||
}
|
||||
|
||||
type NoOpAuthenticator struct{}
|
||||
|
||||
func (NoOpAuthenticator) NonceSize() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (NoOpAuthenticator) Overhead() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Seal implements AEAD.Seal().
|
||||
func (NoOpAuthenticator) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
|
||||
return append(dst[:0], plaintext...)
|
||||
}
|
||||
|
||||
// Open implements AEAD.Open().
|
||||
func (NoOpAuthenticator) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
|
||||
return append(dst[:0], ciphertext...), nil
|
||||
}
|
||||
|
||||
// FnvAuthenticator is an AEAD based on Fnv hash.
|
||||
type FnvAuthenticator struct {
|
||||
}
|
||||
|
||||
// NonceSize implements AEAD.NonceSize().
|
||||
func (*FnvAuthenticator) NonceSize() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Overhead impelements AEAD.Overhead().
|
||||
func (*FnvAuthenticator) Overhead() int {
|
||||
return 4
|
||||
}
|
||||
|
||||
// Seal implements AEAD.Seal().
|
||||
func (*FnvAuthenticator) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
|
||||
dst = append(dst, 0, 0, 0, 0)
|
||||
binary.BigEndian.PutUint32(dst, Authenticate(plaintext))
|
||||
return append(dst, plaintext...)
|
||||
}
|
||||
|
||||
// Open implements AEAD.Open().
|
||||
func (*FnvAuthenticator) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
|
||||
if binary.BigEndian.Uint32(ciphertext[:4]) != Authenticate(ciphertext[4:]) {
|
||||
return dst, newError("invalid authentication")
|
||||
}
|
||||
return append(dst, ciphertext[4:]...), nil
|
||||
}
|
||||
|
||||
// GenerateChacha20Poly1305Key generates a 32-byte key from a given 16-byte array.
|
||||
func GenerateChacha20Poly1305Key(b []byte) []byte {
|
||||
key := make([]byte, 32)
|
||||
t := md5.Sum(b)
|
||||
copy(key, t[:])
|
||||
t = md5.Sum(key[:16])
|
||||
copy(key[16:], t[:])
|
||||
return key
|
||||
}
|
||||
|
||||
type ShakeSizeParser struct {
|
||||
shake sha3.ShakeHash
|
||||
buffer [2]byte
|
||||
}
|
||||
|
||||
func NewShakeSizeParser(nonce []byte) *ShakeSizeParser {
|
||||
shake := sha3.NewShake128()
|
||||
common.Must2(shake.Write(nonce))
|
||||
return &ShakeSizeParser{
|
||||
shake: shake,
|
||||
}
|
||||
}
|
||||
|
||||
func (*ShakeSizeParser) SizeBytes() int32 {
|
||||
return 2
|
||||
}
|
||||
|
||||
func (s *ShakeSizeParser) next() uint16 {
|
||||
common.Must2(s.shake.Read(s.buffer[:]))
|
||||
return binary.BigEndian.Uint16(s.buffer[:])
|
||||
}
|
||||
|
||||
func (s *ShakeSizeParser) Decode(b []byte) (uint16, error) {
|
||||
mask := s.next()
|
||||
size := binary.BigEndian.Uint16(b)
|
||||
return mask ^ size, nil
|
||||
}
|
||||
|
||||
func (s *ShakeSizeParser) Encode(size uint16, b []byte) []byte {
|
||||
mask := s.next()
|
||||
binary.BigEndian.PutUint16(b, mask^size)
|
||||
return b[:2]
|
||||
}
|
||||
|
||||
func (s *ShakeSizeParser) NextPaddingLen() uint16 {
|
||||
return s.next() % 64
|
||||
}
|
||||
|
||||
func (s *ShakeSizeParser) MaxPaddingLen() uint16 {
|
||||
return 64
|
||||
}
|
27
proxy/vmess/encoding/auth_test.go
Normal file
27
proxy/vmess/encoding/auth_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package encoding_test
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
. "github.com/xtls/xray-core/v1/proxy/vmess/encoding"
|
||||
)
|
||||
|
||||
func TestFnvAuth(t *testing.T) {
|
||||
fnvAuth := new(FnvAuthenticator)
|
||||
|
||||
expectedText := make([]byte, 256)
|
||||
_, err := rand.Read(expectedText)
|
||||
common.Must(err)
|
||||
|
||||
buffer := make([]byte, 512)
|
||||
b := fnvAuth.Seal(buffer[:0], nil, expectedText, nil)
|
||||
b, err = fnvAuth.Open(buffer[:0], nil, b, nil)
|
||||
common.Must(err)
|
||||
if r := cmp.Diff(b, expectedText); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
338
proxy/vmess/encoding/client.go
Normal file
338
proxy/vmess/encoding/client.go
Normal file
|
@ -0,0 +1,338 @@
|
|||
package encoding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"hash"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/bitmask"
|
||||
"github.com/xtls/xray-core/v1/common/buf"
|
||||
"github.com/xtls/xray-core/v1/common/crypto"
|
||||
"github.com/xtls/xray-core/v1/common/dice"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess"
|
||||
vmessaead "github.com/xtls/xray-core/v1/proxy/vmess/aead"
|
||||
)
|
||||
|
||||
func hashTimestamp(h hash.Hash, t protocol.Timestamp) []byte {
|
||||
common.Must2(serial.WriteUint64(h, uint64(t)))
|
||||
common.Must2(serial.WriteUint64(h, uint64(t)))
|
||||
common.Must2(serial.WriteUint64(h, uint64(t)))
|
||||
common.Must2(serial.WriteUint64(h, uint64(t)))
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// ClientSession stores connection session info for VMess client.
|
||||
type ClientSession struct {
|
||||
isAEAD bool
|
||||
idHash protocol.IDHash
|
||||
requestBodyKey [16]byte
|
||||
requestBodyIV [16]byte
|
||||
responseBodyKey [16]byte
|
||||
responseBodyIV [16]byte
|
||||
responseReader io.Reader
|
||||
responseHeader byte
|
||||
}
|
||||
|
||||
// NewClientSession creates a new ClientSession.
|
||||
func NewClientSession(ctx context.Context, isAEAD bool, idHash protocol.IDHash) *ClientSession {
|
||||
session := &ClientSession{
|
||||
isAEAD: isAEAD,
|
||||
idHash: idHash,
|
||||
}
|
||||
|
||||
randomBytes := make([]byte, 33) // 16 + 16 + 1
|
||||
common.Must2(rand.Read(randomBytes))
|
||||
copy(session.requestBodyKey[:], randomBytes[:16])
|
||||
copy(session.requestBodyIV[:], randomBytes[16:32])
|
||||
session.responseHeader = randomBytes[32]
|
||||
|
||||
if !session.isAEAD {
|
||||
session.responseBodyKey = md5.Sum(session.requestBodyKey[:])
|
||||
session.responseBodyIV = md5.Sum(session.requestBodyIV[:])
|
||||
} else {
|
||||
BodyKey := sha256.Sum256(session.requestBodyKey[:])
|
||||
copy(session.responseBodyKey[:], BodyKey[:16])
|
||||
BodyIV := sha256.Sum256(session.requestBodyIV[:])
|
||||
copy(session.responseBodyIV[:], BodyIV[:16])
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
func (c *ClientSession) EncodeRequestHeader(header *protocol.RequestHeader, writer io.Writer) error {
|
||||
timestamp := protocol.NewTimestampGenerator(protocol.NowTime(), 30)()
|
||||
account := header.User.Account.(*vmess.MemoryAccount)
|
||||
if !c.isAEAD {
|
||||
idHash := c.idHash(account.AnyValidID().Bytes())
|
||||
common.Must2(serial.WriteUint64(idHash, uint64(timestamp)))
|
||||
common.Must2(writer.Write(idHash.Sum(nil)))
|
||||
}
|
||||
|
||||
buffer := buf.New()
|
||||
defer buffer.Release()
|
||||
|
||||
common.Must(buffer.WriteByte(Version))
|
||||
common.Must2(buffer.Write(c.requestBodyIV[:]))
|
||||
common.Must2(buffer.Write(c.requestBodyKey[:]))
|
||||
common.Must(buffer.WriteByte(c.responseHeader))
|
||||
common.Must(buffer.WriteByte(byte(header.Option)))
|
||||
|
||||
paddingLen := dice.Roll(16)
|
||||
security := byte(paddingLen<<4) | byte(header.Security)
|
||||
common.Must2(buffer.Write([]byte{security, byte(0), byte(header.Command)}))
|
||||
|
||||
if header.Command != protocol.RequestCommandMux {
|
||||
if err := addrParser.WriteAddressPort(buffer, header.Address, header.Port); err != nil {
|
||||
return newError("failed to writer address and port").Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
if paddingLen > 0 {
|
||||
common.Must2(buffer.ReadFullFrom(rand.Reader, int32(paddingLen)))
|
||||
}
|
||||
|
||||
{
|
||||
fnv1a := fnv.New32a()
|
||||
common.Must2(fnv1a.Write(buffer.Bytes()))
|
||||
hashBytes := buffer.Extend(int32(fnv1a.Size()))
|
||||
fnv1a.Sum(hashBytes[:0])
|
||||
}
|
||||
|
||||
if !c.isAEAD {
|
||||
iv := hashTimestamp(md5.New(), timestamp)
|
||||
aesStream := crypto.NewAesEncryptionStream(account.ID.CmdKey(), iv)
|
||||
aesStream.XORKeyStream(buffer.Bytes(), buffer.Bytes())
|
||||
common.Must2(writer.Write(buffer.Bytes()))
|
||||
} else {
|
||||
var fixedLengthCmdKey [16]byte
|
||||
copy(fixedLengthCmdKey[:], account.ID.CmdKey())
|
||||
vmessout := vmessaead.SealVMessAEADHeader(fixedLengthCmdKey, buffer.Bytes())
|
||||
common.Must2(io.Copy(writer, bytes.NewReader(vmessout)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClientSession) EncodeRequestBody(request *protocol.RequestHeader, writer io.Writer) buf.Writer {
|
||||
var sizeParser crypto.ChunkSizeEncoder = crypto.PlainChunkSizeParser{}
|
||||
if request.Option.Has(protocol.RequestOptionChunkMasking) {
|
||||
sizeParser = NewShakeSizeParser(c.requestBodyIV[:])
|
||||
}
|
||||
var padding crypto.PaddingLengthGenerator
|
||||
if request.Option.Has(protocol.RequestOptionGlobalPadding) {
|
||||
padding = sizeParser.(crypto.PaddingLengthGenerator)
|
||||
}
|
||||
|
||||
switch request.Security {
|
||||
case protocol.SecurityType_NONE:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
if request.Command.TransferType() == protocol.TransferTypeStream {
|
||||
return crypto.NewChunkStreamWriter(sizeParser, writer)
|
||||
}
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(NoOpAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding)
|
||||
}
|
||||
|
||||
return buf.NewWriter(writer)
|
||||
case protocol.SecurityType_LEGACY:
|
||||
aesStream := crypto.NewAesEncryptionStream(c.requestBodyKey[:], c.requestBodyIV[:])
|
||||
cryptionWriter := crypto.NewCryptionWriter(aesStream, writer)
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(FnvAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, cryptionWriter, request.Command.TransferType(), padding)
|
||||
}
|
||||
|
||||
return &buf.SequentialWriter{Writer: cryptionWriter}
|
||||
case protocol.SecurityType_AES128_GCM:
|
||||
aead := crypto.NewAesGcm(c.requestBodyKey[:])
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding)
|
||||
case protocol.SecurityType_CHACHA20_POLY1305:
|
||||
aead, err := chacha20poly1305.New(GenerateChacha20Poly1305Key(c.requestBodyKey[:]))
|
||||
common.Must(err)
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding)
|
||||
default:
|
||||
panic("Unknown security type.")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ClientSession) DecodeResponseHeader(reader io.Reader) (*protocol.ResponseHeader, error) {
|
||||
if !c.isAEAD {
|
||||
aesStream := crypto.NewAesDecryptionStream(c.responseBodyKey[:], c.responseBodyIV[:])
|
||||
c.responseReader = crypto.NewCryptionReader(aesStream, reader)
|
||||
} else {
|
||||
aeadResponseHeaderLengthEncryptionKey := vmessaead.KDF16(c.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderLenKey)
|
||||
aeadResponseHeaderLengthEncryptionIV := vmessaead.KDF(c.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderLenIV)[:12]
|
||||
|
||||
aeadResponseHeaderLengthEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderLengthEncryptionKey)).(cipher.Block)
|
||||
aeadResponseHeaderLengthEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderLengthEncryptionKeyAESBlock)).(cipher.AEAD)
|
||||
|
||||
var aeadEncryptedResponseHeaderLength [18]byte
|
||||
var decryptedResponseHeaderLength int
|
||||
var decryptedResponseHeaderLengthBinaryDeserializeBuffer uint16
|
||||
|
||||
if _, err := io.ReadFull(reader, aeadEncryptedResponseHeaderLength[:]); err != nil {
|
||||
return nil, newError("Unable to Read Header Len").Base(err)
|
||||
}
|
||||
if decryptedResponseHeaderLengthBinaryBuffer, err := aeadResponseHeaderLengthEncryptionAEAD.Open(nil, aeadResponseHeaderLengthEncryptionIV, aeadEncryptedResponseHeaderLength[:], nil); err != nil {
|
||||
return nil, newError("Failed To Decrypt Length").Base(err)
|
||||
} else {
|
||||
common.Must(binary.Read(bytes.NewReader(decryptedResponseHeaderLengthBinaryBuffer), binary.BigEndian, &decryptedResponseHeaderLengthBinaryDeserializeBuffer))
|
||||
decryptedResponseHeaderLength = int(decryptedResponseHeaderLengthBinaryDeserializeBuffer)
|
||||
}
|
||||
|
||||
aeadResponseHeaderPayloadEncryptionKey := vmessaead.KDF16(c.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadKey)
|
||||
aeadResponseHeaderPayloadEncryptionIV := vmessaead.KDF(c.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadIV)[:12]
|
||||
|
||||
aeadResponseHeaderPayloadEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderPayloadEncryptionKey)).(cipher.Block)
|
||||
aeadResponseHeaderPayloadEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderPayloadEncryptionKeyAESBlock)).(cipher.AEAD)
|
||||
|
||||
encryptedResponseHeaderBuffer := make([]byte, decryptedResponseHeaderLength+16)
|
||||
|
||||
if _, err := io.ReadFull(reader, encryptedResponseHeaderBuffer); err != nil {
|
||||
return nil, newError("Unable to Read Header Data").Base(err)
|
||||
}
|
||||
|
||||
if decryptedResponseHeaderBuffer, err := aeadResponseHeaderPayloadEncryptionAEAD.Open(nil, aeadResponseHeaderPayloadEncryptionIV, encryptedResponseHeaderBuffer, nil); err != nil {
|
||||
return nil, newError("Failed To Decrypt Payload").Base(err)
|
||||
} else {
|
||||
c.responseReader = bytes.NewReader(decryptedResponseHeaderBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
buffer := buf.StackNew()
|
||||
defer buffer.Release()
|
||||
|
||||
if _, err := buffer.ReadFullFrom(c.responseReader, 4); err != nil {
|
||||
return nil, newError("failed to read response header").Base(err).AtWarning()
|
||||
}
|
||||
|
||||
if buffer.Byte(0) != c.responseHeader {
|
||||
return nil, newError("unexpected response header. Expecting ", int(c.responseHeader), " but actually ", int(buffer.Byte(0)))
|
||||
}
|
||||
|
||||
header := &protocol.ResponseHeader{
|
||||
Option: bitmask.Byte(buffer.Byte(1)),
|
||||
}
|
||||
|
||||
if buffer.Byte(2) != 0 {
|
||||
cmdID := buffer.Byte(2)
|
||||
dataLen := int32(buffer.Byte(3))
|
||||
|
||||
buffer.Clear()
|
||||
if _, err := buffer.ReadFullFrom(c.responseReader, dataLen); err != nil {
|
||||
return nil, newError("failed to read response command").Base(err)
|
||||
}
|
||||
command, err := UnmarshalCommand(cmdID, buffer.Bytes())
|
||||
if err == nil {
|
||||
header.Command = command
|
||||
}
|
||||
}
|
||||
if c.isAEAD {
|
||||
aesStream := crypto.NewAesDecryptionStream(c.responseBodyKey[:], c.responseBodyIV[:])
|
||||
c.responseReader = crypto.NewCryptionReader(aesStream, reader)
|
||||
}
|
||||
return header, nil
|
||||
}
|
||||
|
||||
func (c *ClientSession) DecodeResponseBody(request *protocol.RequestHeader, reader io.Reader) buf.Reader {
|
||||
var sizeParser crypto.ChunkSizeDecoder = crypto.PlainChunkSizeParser{}
|
||||
if request.Option.Has(protocol.RequestOptionChunkMasking) {
|
||||
sizeParser = NewShakeSizeParser(c.responseBodyIV[:])
|
||||
}
|
||||
var padding crypto.PaddingLengthGenerator
|
||||
if request.Option.Has(protocol.RequestOptionGlobalPadding) {
|
||||
padding = sizeParser.(crypto.PaddingLengthGenerator)
|
||||
}
|
||||
|
||||
switch request.Security {
|
||||
case protocol.SecurityType_NONE:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
if request.Command.TransferType() == protocol.TransferTypeStream {
|
||||
return crypto.NewChunkStreamReader(sizeParser, reader)
|
||||
}
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(NoOpAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding)
|
||||
}
|
||||
|
||||
return buf.NewReader(reader)
|
||||
case protocol.SecurityType_LEGACY:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(FnvAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, c.responseReader, request.Command.TransferType(), padding)
|
||||
}
|
||||
|
||||
return buf.NewReader(c.responseReader)
|
||||
case protocol.SecurityType_AES128_GCM:
|
||||
aead := crypto.NewAesGcm(c.responseBodyKey[:])
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(c.responseBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding)
|
||||
case protocol.SecurityType_CHACHA20_POLY1305:
|
||||
aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(c.responseBodyKey[:]))
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(c.responseBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding)
|
||||
default:
|
||||
panic("Unknown security type.")
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateChunkNonce(nonce []byte, size uint32) crypto.BytesGenerator {
|
||||
c := append([]byte(nil), nonce...)
|
||||
count := uint16(0)
|
||||
return func() []byte {
|
||||
binary.BigEndian.PutUint16(c, count)
|
||||
count++
|
||||
return c[:size]
|
||||
}
|
||||
}
|
148
proxy/vmess/encoding/commands.go
Normal file
148
proxy/vmess/encoding/commands.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
package encoding
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/buf"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/serial"
|
||||
"github.com/xtls/xray-core/v1/common/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCommandTypeMismatch = newError("Command type mismatch.")
|
||||
ErrUnknownCommand = newError("Unknown command.")
|
||||
ErrCommandTooLarge = newError("Command too large.")
|
||||
)
|
||||
|
||||
func MarshalCommand(command interface{}, writer io.Writer) error {
|
||||
if command == nil {
|
||||
return ErrUnknownCommand
|
||||
}
|
||||
|
||||
var cmdID byte
|
||||
var factory CommandFactory
|
||||
switch command.(type) {
|
||||
case *protocol.CommandSwitchAccount:
|
||||
factory = new(CommandSwitchAccountFactory)
|
||||
cmdID = 1
|
||||
default:
|
||||
return ErrUnknownCommand
|
||||
}
|
||||
|
||||
buffer := buf.New()
|
||||
defer buffer.Release()
|
||||
|
||||
err := factory.Marshal(command, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
auth := Authenticate(buffer.Bytes())
|
||||
length := buffer.Len() + 4
|
||||
if length > 255 {
|
||||
return ErrCommandTooLarge
|
||||
}
|
||||
|
||||
common.Must2(writer.Write([]byte{cmdID, byte(length), byte(auth >> 24), byte(auth >> 16), byte(auth >> 8), byte(auth)}))
|
||||
common.Must2(writer.Write(buffer.Bytes()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnmarshalCommand(cmdID byte, data []byte) (protocol.ResponseCommand, error) {
|
||||
if len(data) <= 4 {
|
||||
return nil, newError("insufficient length")
|
||||
}
|
||||
expectedAuth := Authenticate(data[4:])
|
||||
actualAuth := binary.BigEndian.Uint32(data[:4])
|
||||
if expectedAuth != actualAuth {
|
||||
return nil, newError("invalid auth")
|
||||
}
|
||||
|
||||
var factory CommandFactory
|
||||
switch cmdID {
|
||||
case 1:
|
||||
factory = new(CommandSwitchAccountFactory)
|
||||
default:
|
||||
return nil, ErrUnknownCommand
|
||||
}
|
||||
return factory.Unmarshal(data[4:])
|
||||
}
|
||||
|
||||
type CommandFactory interface {
|
||||
Marshal(command interface{}, writer io.Writer) error
|
||||
Unmarshal(data []byte) (interface{}, error)
|
||||
}
|
||||
|
||||
type CommandSwitchAccountFactory struct {
|
||||
}
|
||||
|
||||
func (f *CommandSwitchAccountFactory) Marshal(command interface{}, writer io.Writer) error {
|
||||
cmd, ok := command.(*protocol.CommandSwitchAccount)
|
||||
if !ok {
|
||||
return ErrCommandTypeMismatch
|
||||
}
|
||||
|
||||
hostStr := ""
|
||||
if cmd.Host != nil {
|
||||
hostStr = cmd.Host.String()
|
||||
}
|
||||
common.Must2(writer.Write([]byte{byte(len(hostStr))}))
|
||||
|
||||
if len(hostStr) > 0 {
|
||||
common.Must2(writer.Write([]byte(hostStr)))
|
||||
}
|
||||
|
||||
common.Must2(serial.WriteUint16(writer, cmd.Port.Value()))
|
||||
|
||||
idBytes := cmd.ID.Bytes()
|
||||
common.Must2(writer.Write(idBytes))
|
||||
common.Must2(serial.WriteUint16(writer, cmd.AlterIds))
|
||||
common.Must2(writer.Write([]byte{byte(cmd.Level)}))
|
||||
|
||||
common.Must2(writer.Write([]byte{cmd.ValidMin}))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *CommandSwitchAccountFactory) Unmarshal(data []byte) (interface{}, error) {
|
||||
cmd := new(protocol.CommandSwitchAccount)
|
||||
if len(data) == 0 {
|
||||
return nil, newError("insufficient length.")
|
||||
}
|
||||
lenHost := int(data[0])
|
||||
if len(data) < lenHost+1 {
|
||||
return nil, newError("insufficient length.")
|
||||
}
|
||||
if lenHost > 0 {
|
||||
cmd.Host = net.ParseAddress(string(data[1 : 1+lenHost]))
|
||||
}
|
||||
portStart := 1 + lenHost
|
||||
if len(data) < portStart+2 {
|
||||
return nil, newError("insufficient length.")
|
||||
}
|
||||
cmd.Port = net.PortFromBytes(data[portStart : portStart+2])
|
||||
idStart := portStart + 2
|
||||
if len(data) < idStart+16 {
|
||||
return nil, newError("insufficient length.")
|
||||
}
|
||||
cmd.ID, _ = uuid.ParseBytes(data[idStart : idStart+16])
|
||||
alterIDStart := idStart + 16
|
||||
if len(data) < alterIDStart+2 {
|
||||
return nil, newError("insufficient length.")
|
||||
}
|
||||
cmd.AlterIds = binary.BigEndian.Uint16(data[alterIDStart : alterIDStart+2])
|
||||
levelStart := alterIDStart + 2
|
||||
if len(data) < levelStart+1 {
|
||||
return nil, newError("insufficient length.")
|
||||
}
|
||||
cmd.Level = uint32(data[levelStart])
|
||||
timeStart := levelStart + 1
|
||||
if len(data) < timeStart {
|
||||
return nil, newError("insufficient length.")
|
||||
}
|
||||
cmd.ValidMin = data[timeStart]
|
||||
return cmd, nil
|
||||
}
|
37
proxy/vmess/encoding/commands_test.go
Normal file
37
proxy/vmess/encoding/commands_test.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package encoding_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/buf"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/uuid"
|
||||
. "github.com/xtls/xray-core/v1/proxy/vmess/encoding"
|
||||
)
|
||||
|
||||
func TestSwitchAccount(t *testing.T) {
|
||||
sa := &protocol.CommandSwitchAccount{
|
||||
Port: 1234,
|
||||
ID: uuid.New(),
|
||||
AlterIds: 1024,
|
||||
Level: 128,
|
||||
ValidMin: 16,
|
||||
}
|
||||
|
||||
buffer := buf.New()
|
||||
common.Must(MarshalCommand(sa, buffer))
|
||||
|
||||
cmd, err := UnmarshalCommand(1, buffer.BytesFrom(2))
|
||||
common.Must(err)
|
||||
|
||||
sa2, ok := cmd.(*protocol.CommandSwitchAccount)
|
||||
if !ok {
|
||||
t.Fatal("failed to convert command to CommandSwitchAccount")
|
||||
}
|
||||
if r := cmp.Diff(sa2, sa); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
19
proxy/vmess/encoding/encoding.go
Normal file
19
proxy/vmess/encoding/encoding.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package encoding
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/xtls/xray-core/v1/common/errors/errorgen
|
||||
|
||||
const (
|
||||
Version = byte(1)
|
||||
)
|
||||
|
||||
var addrParser = protocol.NewAddressParser(
|
||||
protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv4), net.AddressFamilyIPv4),
|
||||
protocol.AddressFamilyByte(byte(protocol.AddressTypeDomain), net.AddressFamilyDomain),
|
||||
protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv6), net.AddressFamilyIPv6),
|
||||
protocol.PortThenAddress(),
|
||||
)
|
157
proxy/vmess/encoding/encoding_test.go
Normal file
157
proxy/vmess/encoding/encoding_test.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
package encoding_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/buf"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/uuid"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess"
|
||||
. "github.com/xtls/xray-core/v1/proxy/vmess/encoding"
|
||||
)
|
||||
|
||||
func toAccount(a *vmess.Account) protocol.Account {
|
||||
account, err := a.AsAccount()
|
||||
common.Must(err)
|
||||
return account
|
||||
}
|
||||
|
||||
func TestRequestSerialization(t *testing.T) {
|
||||
user := &protocol.MemoryUser{
|
||||
Level: 0,
|
||||
Email: "test@example.com",
|
||||
}
|
||||
id := uuid.New()
|
||||
account := &vmess.Account{
|
||||
Id: id.String(),
|
||||
AlterId: 0,
|
||||
}
|
||||
user.Account = toAccount(account)
|
||||
|
||||
expectedRequest := &protocol.RequestHeader{
|
||||
Version: 1,
|
||||
User: user,
|
||||
Command: protocol.RequestCommandTCP,
|
||||
Address: net.DomainAddress("www.example.com"),
|
||||
Port: net.Port(443),
|
||||
Security: protocol.SecurityType_AES128_GCM,
|
||||
}
|
||||
|
||||
buffer := buf.New()
|
||||
client := NewClientSession(context.TODO(), true, protocol.DefaultIDHash)
|
||||
common.Must(client.EncodeRequestHeader(expectedRequest, buffer))
|
||||
|
||||
buffer2 := buf.New()
|
||||
buffer2.Write(buffer.Bytes())
|
||||
|
||||
sessionHistory := NewSessionHistory()
|
||||
defer common.Close(sessionHistory)
|
||||
|
||||
userValidator := vmess.NewTimedUserValidator(protocol.DefaultIDHash)
|
||||
userValidator.Add(user)
|
||||
defer common.Close(userValidator)
|
||||
|
||||
server := NewServerSession(userValidator, sessionHistory)
|
||||
actualRequest, err := server.DecodeRequestHeader(buffer)
|
||||
common.Must(err)
|
||||
|
||||
if r := cmp.Diff(actualRequest, expectedRequest, cmp.AllowUnexported(protocol.ID{})); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
|
||||
_, err = server.DecodeRequestHeader(buffer2)
|
||||
// anti replay attack
|
||||
if err == nil {
|
||||
t.Error("nil error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidRequest(t *testing.T) {
|
||||
user := &protocol.MemoryUser{
|
||||
Level: 0,
|
||||
Email: "test@example.com",
|
||||
}
|
||||
id := uuid.New()
|
||||
account := &vmess.Account{
|
||||
Id: id.String(),
|
||||
AlterId: 0,
|
||||
}
|
||||
user.Account = toAccount(account)
|
||||
|
||||
expectedRequest := &protocol.RequestHeader{
|
||||
Version: 1,
|
||||
User: user,
|
||||
Command: protocol.RequestCommand(100),
|
||||
Address: net.DomainAddress("www.example.com"),
|
||||
Port: net.Port(443),
|
||||
Security: protocol.SecurityType_AES128_GCM,
|
||||
}
|
||||
|
||||
buffer := buf.New()
|
||||
client := NewClientSession(context.TODO(), true, protocol.DefaultIDHash)
|
||||
common.Must(client.EncodeRequestHeader(expectedRequest, buffer))
|
||||
|
||||
buffer2 := buf.New()
|
||||
buffer2.Write(buffer.Bytes())
|
||||
|
||||
sessionHistory := NewSessionHistory()
|
||||
defer common.Close(sessionHistory)
|
||||
|
||||
userValidator := vmess.NewTimedUserValidator(protocol.DefaultIDHash)
|
||||
userValidator.Add(user)
|
||||
defer common.Close(userValidator)
|
||||
|
||||
server := NewServerSession(userValidator, sessionHistory)
|
||||
_, err := server.DecodeRequestHeader(buffer)
|
||||
if err == nil {
|
||||
t.Error("nil error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMuxRequest(t *testing.T) {
|
||||
user := &protocol.MemoryUser{
|
||||
Level: 0,
|
||||
Email: "test@example.com",
|
||||
}
|
||||
id := uuid.New()
|
||||
account := &vmess.Account{
|
||||
Id: id.String(),
|
||||
AlterId: 0,
|
||||
}
|
||||
user.Account = toAccount(account)
|
||||
|
||||
expectedRequest := &protocol.RequestHeader{
|
||||
Version: 1,
|
||||
User: user,
|
||||
Command: protocol.RequestCommandMux,
|
||||
Security: protocol.SecurityType_AES128_GCM,
|
||||
Address: net.DomainAddress("v1.mux.cool"),
|
||||
}
|
||||
|
||||
buffer := buf.New()
|
||||
client := NewClientSession(context.TODO(), true, protocol.DefaultIDHash)
|
||||
common.Must(client.EncodeRequestHeader(expectedRequest, buffer))
|
||||
|
||||
buffer2 := buf.New()
|
||||
buffer2.Write(buffer.Bytes())
|
||||
|
||||
sessionHistory := NewSessionHistory()
|
||||
defer common.Close(sessionHistory)
|
||||
|
||||
userValidator := vmess.NewTimedUserValidator(protocol.DefaultIDHash)
|
||||
userValidator.Add(user)
|
||||
defer common.Close(userValidator)
|
||||
|
||||
server := NewServerSession(userValidator, sessionHistory)
|
||||
actualRequest, err := server.DecodeRequestHeader(buffer)
|
||||
common.Must(err)
|
||||
|
||||
if r := cmp.Diff(actualRequest, expectedRequest, cmp.AllowUnexported(protocol.ID{})); r != "" {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
9
proxy/vmess/encoding/errors.generated.go
Normal file
9
proxy/vmess/encoding/errors.generated.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package encoding
|
||||
|
||||
import "github.com/xtls/xray-core/v1/common/errors"
|
||||
|
||||
type errPathObjHolder struct{}
|
||||
|
||||
func newError(values ...interface{}) *errors.Error {
|
||||
return errors.New(values...).WithPathObj(errPathObjHolder{})
|
||||
}
|
492
proxy/vmess/encoding/server.go
Normal file
492
proxy/vmess/encoding/server.go
Normal file
|
@ -0,0 +1,492 @@
|
|||
package encoding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/v1/common"
|
||||
"github.com/xtls/xray-core/v1/common/bitmask"
|
||||
"github.com/xtls/xray-core/v1/common/buf"
|
||||
"github.com/xtls/xray-core/v1/common/crypto"
|
||||
"github.com/xtls/xray-core/v1/common/dice"
|
||||
"github.com/xtls/xray-core/v1/common/net"
|
||||
"github.com/xtls/xray-core/v1/common/protocol"
|
||||
"github.com/xtls/xray-core/v1/common/task"
|
||||
"github.com/xtls/xray-core/v1/proxy/vmess"
|
||||
vmessaead "github.com/xtls/xray-core/v1/proxy/vmess/aead"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
type sessionID struct {
|
||||
user [16]byte
|
||||
key [16]byte
|
||||
nonce [16]byte
|
||||
}
|
||||
|
||||
// SessionHistory keeps track of historical session ids, to prevent replay attacks.
|
||||
type SessionHistory struct {
|
||||
sync.RWMutex
|
||||
cache map[sessionID]time.Time
|
||||
task *task.Periodic
|
||||
}
|
||||
|
||||
// NewSessionHistory creates a new SessionHistory object.
|
||||
func NewSessionHistory() *SessionHistory {
|
||||
h := &SessionHistory{
|
||||
cache: make(map[sessionID]time.Time, 128),
|
||||
}
|
||||
h.task = &task.Periodic{
|
||||
Interval: time.Second * 30,
|
||||
Execute: h.removeExpiredEntries,
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Close implements common.Closable.
|
||||
func (h *SessionHistory) Close() error {
|
||||
return h.task.Close()
|
||||
}
|
||||
|
||||
func (h *SessionHistory) addIfNotExits(session sessionID) bool {
|
||||
h.Lock()
|
||||
|
||||
if expire, found := h.cache[session]; found && expire.After(time.Now()) {
|
||||
h.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
h.cache[session] = time.Now().Add(time.Minute * 3)
|
||||
h.Unlock()
|
||||
common.Must(h.task.Start())
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *SessionHistory) removeExpiredEntries() error {
|
||||
now := time.Now()
|
||||
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
if len(h.cache) == 0 {
|
||||
return newError("nothing to do")
|
||||
}
|
||||
|
||||
for session, expire := range h.cache {
|
||||
if expire.Before(now) {
|
||||
delete(h.cache, session)
|
||||
}
|
||||
}
|
||||
|
||||
if len(h.cache) == 0 {
|
||||
h.cache = make(map[sessionID]time.Time, 128)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerSession keeps information for a session in VMess server.
|
||||
type ServerSession struct {
|
||||
userValidator *vmess.TimedUserValidator
|
||||
sessionHistory *SessionHistory
|
||||
requestBodyKey [16]byte
|
||||
requestBodyIV [16]byte
|
||||
responseBodyKey [16]byte
|
||||
responseBodyIV [16]byte
|
||||
responseWriter io.Writer
|
||||
responseHeader byte
|
||||
|
||||
isAEADRequest bool
|
||||
|
||||
isAEADForced bool
|
||||
}
|
||||
|
||||
// NewServerSession creates a new ServerSession, using the given UserValidator.
|
||||
// The ServerSession instance doesn't take ownership of the validator.
|
||||
func NewServerSession(validator *vmess.TimedUserValidator, sessionHistory *SessionHistory) *ServerSession {
|
||||
return &ServerSession{
|
||||
userValidator: validator,
|
||||
sessionHistory: sessionHistory,
|
||||
}
|
||||
}
|
||||
|
||||
func parseSecurityType(b byte) protocol.SecurityType {
|
||||
if _, f := protocol.SecurityType_name[int32(b)]; f {
|
||||
st := protocol.SecurityType(b)
|
||||
// For backward compatibility.
|
||||
if st == protocol.SecurityType_UNKNOWN {
|
||||
st = protocol.SecurityType_LEGACY
|
||||
}
|
||||
return st
|
||||
}
|
||||
return protocol.SecurityType_UNKNOWN
|
||||
}
|
||||
|
||||
// DecodeRequestHeader decodes and returns (if successful) a RequestHeader from an input stream.
|
||||
func (s *ServerSession) DecodeRequestHeader(reader io.Reader) (*protocol.RequestHeader, error) {
|
||||
buffer := buf.New()
|
||||
behaviorRand := dice.NewDeterministicDice(int64(s.userValidator.GetBehaviorSeed()))
|
||||
BaseDrainSize := behaviorRand.Roll(3266)
|
||||
RandDrainMax := behaviorRand.Roll(64) + 1
|
||||
RandDrainRolled := dice.Roll(RandDrainMax)
|
||||
DrainSize := BaseDrainSize + 16 + 38 + RandDrainRolled
|
||||
readSizeRemain := DrainSize
|
||||
|
||||
drainConnection := func(e error) error {
|
||||
// We read a deterministic generated length of data before closing the connection to offset padding read pattern
|
||||
readSizeRemain -= int(buffer.Len())
|
||||
if readSizeRemain > 0 {
|
||||
err := s.DrainConnN(reader, readSizeRemain)
|
||||
if err != nil {
|
||||
return newError("failed to drain connection DrainSize = ", BaseDrainSize, " ", RandDrainMax, " ", RandDrainRolled).Base(err).Base(e)
|
||||
}
|
||||
return newError("connection drained DrainSize = ", BaseDrainSize, " ", RandDrainMax, " ", RandDrainRolled).Base(e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
defer func() {
|
||||
buffer.Release()
|
||||
}()
|
||||
|
||||
if _, err := buffer.ReadFullFrom(reader, protocol.IDBytesLen); err != nil {
|
||||
return nil, newError("failed to read request header").Base(err)
|
||||
}
|
||||
|
||||
var decryptor io.Reader
|
||||
var vmessAccount *vmess.MemoryAccount
|
||||
|
||||
user, foundAEAD, errorAEAD := s.userValidator.GetAEAD(buffer.Bytes())
|
||||
|
||||
var fixedSizeAuthID [16]byte
|
||||
copy(fixedSizeAuthID[:], buffer.Bytes())
|
||||
|
||||
switch {
|
||||
case foundAEAD:
|
||||
vmessAccount = user.Account.(*vmess.MemoryAccount)
|
||||
var fixedSizeCmdKey [16]byte
|
||||
copy(fixedSizeCmdKey[:], vmessAccount.ID.CmdKey())
|
||||
aeadData, shouldDrain, bytesRead, errorReason := vmessaead.OpenVMessAEADHeader(fixedSizeCmdKey, fixedSizeAuthID, reader)
|
||||
if errorReason != nil {
|
||||
if shouldDrain {
|
||||
readSizeRemain -= bytesRead
|
||||
return nil, drainConnection(newError("AEAD read failed").Base(errorReason))
|
||||
} else {
|
||||
return nil, drainConnection(newError("AEAD read failed, drain skipped").Base(errorReason))
|
||||
}
|
||||
}
|
||||
decryptor = bytes.NewReader(aeadData)
|
||||
s.isAEADRequest = true
|
||||
|
||||
case !s.isAEADForced && errorAEAD == vmessaead.ErrNotFound:
|
||||
userLegacy, timestamp, valid, userValidationError := s.userValidator.Get(buffer.Bytes())
|
||||
if !valid || userValidationError != nil {
|
||||
return nil, drainConnection(newError("invalid user").Base(userValidationError))
|
||||
}
|
||||
user = userLegacy
|
||||
iv := hashTimestamp(md5.New(), timestamp)
|
||||
vmessAccount = userLegacy.Account.(*vmess.MemoryAccount)
|
||||
|
||||
aesStream := crypto.NewAesDecryptionStream(vmessAccount.ID.CmdKey(), iv)
|
||||
decryptor = crypto.NewCryptionReader(aesStream, reader)
|
||||
|
||||
default:
|
||||
return nil, drainConnection(newError("invalid user").Base(errorAEAD))
|
||||
}
|
||||
|
||||
readSizeRemain -= int(buffer.Len())
|
||||
buffer.Clear()
|
||||
if _, err := buffer.ReadFullFrom(decryptor, 38); err != nil {
|
||||
return nil, newError("failed to read request header").Base(err)
|
||||
}
|
||||
|
||||
request := &protocol.RequestHeader{
|
||||
User: user,
|
||||
Version: buffer.Byte(0),
|
||||
}
|
||||
|
||||
copy(s.requestBodyIV[:], buffer.BytesRange(1, 17)) // 16 bytes
|
||||
copy(s.requestBodyKey[:], buffer.BytesRange(17, 33)) // 16 bytes
|
||||
var sid sessionID
|
||||
copy(sid.user[:], vmessAccount.ID.Bytes())
|
||||
sid.key = s.requestBodyKey
|
||||
sid.nonce = s.requestBodyIV
|
||||
if !s.sessionHistory.addIfNotExits(sid) {
|
||||
if !s.isAEADRequest {
|
||||
drainErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:])
|
||||
if drainErr != nil {
|
||||
return nil, drainConnection(newError("duplicated session id, possibly under replay attack, and failed to taint userHash").Base(drainErr))
|
||||
}
|
||||
return nil, drainConnection(newError("duplicated session id, possibly under replay attack, userHash tainted"))
|
||||
} else {
|
||||
return nil, newError("duplicated session id, possibly under replay attack, but this is a AEAD request")
|
||||
}
|
||||
}
|
||||
|
||||
s.responseHeader = buffer.Byte(33) // 1 byte
|
||||
request.Option = bitmask.Byte(buffer.Byte(34)) // 1 byte
|
||||
paddingLen := int(buffer.Byte(35) >> 4)
|
||||
request.Security = parseSecurityType(buffer.Byte(35) & 0x0F)
|
||||
// 1 bytes reserved
|
||||
request.Command = protocol.RequestCommand(buffer.Byte(37))
|
||||
|
||||
switch request.Command {
|
||||
case protocol.RequestCommandMux:
|
||||
request.Address = net.DomainAddress("v1.mux.cool")
|
||||
request.Port = 0
|
||||
|
||||
case protocol.RequestCommandTCP, protocol.RequestCommandUDP:
|
||||
if addr, port, err := addrParser.ReadAddressPort(buffer, decryptor); err == nil {
|
||||
request.Address = addr
|
||||
request.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
if paddingLen > 0 {
|
||||
if _, err := buffer.ReadFullFrom(decryptor, int32(paddingLen)); err != nil {
|
||||
if !s.isAEADRequest {
|
||||
burnErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:])
|
||||
if burnErr != nil {
|
||||
return nil, newError("failed to read padding, failed to taint userHash").Base(burnErr).Base(err)
|
||||
}
|
||||
return nil, newError("failed to read padding, userHash tainted").Base(err)
|
||||
}
|
||||
return nil, newError("failed to read padding").Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := buffer.ReadFullFrom(decryptor, 4); err != nil {
|
||||
if !s.isAEADRequest {
|
||||
burnErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:])
|
||||
if burnErr != nil {
|
||||
return nil, newError("failed to read checksum, failed to taint userHash").Base(burnErr).Base(err)
|
||||
}
|
||||
return nil, newError("failed to read checksum, userHash tainted").Base(err)
|
||||
}
|
||||
return nil, newError("failed to read checksum").Base(err)
|
||||
}
|
||||
|
||||
fnv1a := fnv.New32a()
|
||||
common.Must2(fnv1a.Write(buffer.BytesTo(-4)))
|
||||
actualHash := fnv1a.Sum32()
|
||||
expectedHash := binary.BigEndian.Uint32(buffer.BytesFrom(-4))
|
||||
|
||||
if actualHash != expectedHash {
|
||||
if !s.isAEADRequest {
|
||||
Autherr := newError("invalid auth, legacy userHash tainted")
|
||||
burnErr := s.userValidator.BurnTaintFuse(fixedSizeAuthID[:])
|
||||
if burnErr != nil {
|
||||
Autherr = newError("invalid auth, can't taint legacy userHash").Base(burnErr)
|
||||
}
|
||||
// It is possible that we are under attack described in https://github.com/xray/xray-core/issues/2523
|
||||
return nil, drainConnection(Autherr)
|
||||
} else {
|
||||
return nil, newError("invalid auth, but this is a AEAD request")
|
||||
}
|
||||
}
|
||||
|
||||
if request.Address == nil {
|
||||
return nil, newError("invalid remote address")
|
||||
}
|
||||
|
||||
if request.Security == protocol.SecurityType_UNKNOWN || request.Security == protocol.SecurityType_AUTO {
|
||||
return nil, newError("unknown security type: ", request.Security)
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
// DecodeRequestBody returns Reader from which caller can fetch decrypted body.
|
||||
func (s *ServerSession) DecodeRequestBody(request *protocol.RequestHeader, reader io.Reader) buf.Reader {
|
||||
var sizeParser crypto.ChunkSizeDecoder = crypto.PlainChunkSizeParser{}
|
||||
if request.Option.Has(protocol.RequestOptionChunkMasking) {
|
||||
sizeParser = NewShakeSizeParser(s.requestBodyIV[:])
|
||||
}
|
||||
var padding crypto.PaddingLengthGenerator
|
||||
if request.Option.Has(protocol.RequestOptionGlobalPadding) {
|
||||
padding = sizeParser.(crypto.PaddingLengthGenerator)
|
||||
}
|
||||
|
||||
switch request.Security {
|
||||
case protocol.SecurityType_NONE:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
if request.Command.TransferType() == protocol.TransferTypeStream {
|
||||
return crypto.NewChunkStreamReader(sizeParser, reader)
|
||||
}
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(NoOpAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding)
|
||||
}
|
||||
return buf.NewReader(reader)
|
||||
|
||||
case protocol.SecurityType_LEGACY:
|
||||
aesStream := crypto.NewAesDecryptionStream(s.requestBodyKey[:], s.requestBodyIV[:])
|
||||
cryptionReader := crypto.NewCryptionReader(aesStream, reader)
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(FnvAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, cryptionReader, request.Command.TransferType(), padding)
|
||||
}
|
||||
return buf.NewReader(cryptionReader)
|
||||
|
||||
case protocol.SecurityType_AES128_GCM:
|
||||
aead := crypto.NewAesGcm(s.requestBodyKey[:])
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(s.requestBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding)
|
||||
|
||||
case protocol.SecurityType_CHACHA20_POLY1305:
|
||||
aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(s.requestBodyKey[:]))
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(s.requestBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding)
|
||||
|
||||
default:
|
||||
panic("Unknown security type.")
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeResponseHeader writes encoded response header into the given writer.
|
||||
func (s *ServerSession) EncodeResponseHeader(header *protocol.ResponseHeader, writer io.Writer) {
|
||||
var encryptionWriter io.Writer
|
||||
if !s.isAEADRequest {
|
||||
s.responseBodyKey = md5.Sum(s.requestBodyKey[:])
|
||||
s.responseBodyIV = md5.Sum(s.requestBodyIV[:])
|
||||
} else {
|
||||
BodyKey := sha256.Sum256(s.requestBodyKey[:])
|
||||
copy(s.responseBodyKey[:], BodyKey[:16])
|
||||
BodyIV := sha256.Sum256(s.requestBodyIV[:])
|
||||
copy(s.responseBodyIV[:], BodyIV[:16])
|
||||
}
|
||||
|
||||
aesStream := crypto.NewAesEncryptionStream(s.responseBodyKey[:], s.responseBodyIV[:])
|
||||
encryptionWriter = crypto.NewCryptionWriter(aesStream, writer)
|
||||
s.responseWriter = encryptionWriter
|
||||
|
||||
aeadEncryptedHeaderBuffer := bytes.NewBuffer(nil)
|
||||
|
||||
if s.isAEADRequest {
|
||||
encryptionWriter = aeadEncryptedHeaderBuffer
|
||||
}
|
||||
|
||||
common.Must2(encryptionWriter.Write([]byte{s.responseHeader, byte(header.Option)}))
|
||||
err := MarshalCommand(header.Command, encryptionWriter)
|
||||
if err != nil {
|
||||
common.Must2(encryptionWriter.Write([]byte{0x00, 0x00}))
|
||||
}
|
||||
|
||||
if s.isAEADRequest {
|
||||
aeadResponseHeaderLengthEncryptionKey := vmessaead.KDF16(s.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderLenKey)
|
||||
aeadResponseHeaderLengthEncryptionIV := vmessaead.KDF(s.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderLenIV)[:12]
|
||||
|
||||
aeadResponseHeaderLengthEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderLengthEncryptionKey)).(cipher.Block)
|
||||
aeadResponseHeaderLengthEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderLengthEncryptionKeyAESBlock)).(cipher.AEAD)
|
||||
|
||||
aeadResponseHeaderLengthEncryptionBuffer := bytes.NewBuffer(nil)
|
||||
|
||||
decryptedResponseHeaderLengthBinaryDeserializeBuffer := uint16(aeadEncryptedHeaderBuffer.Len())
|
||||
|
||||
common.Must(binary.Write(aeadResponseHeaderLengthEncryptionBuffer, binary.BigEndian, decryptedResponseHeaderLengthBinaryDeserializeBuffer))
|
||||
|
||||
AEADEncryptedLength := aeadResponseHeaderLengthEncryptionAEAD.Seal(nil, aeadResponseHeaderLengthEncryptionIV, aeadResponseHeaderLengthEncryptionBuffer.Bytes(), nil)
|
||||
common.Must2(io.Copy(writer, bytes.NewReader(AEADEncryptedLength)))
|
||||
|
||||
aeadResponseHeaderPayloadEncryptionKey := vmessaead.KDF16(s.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadKey)
|
||||
aeadResponseHeaderPayloadEncryptionIV := vmessaead.KDF(s.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadIV)[:12]
|
||||
|
||||
aeadResponseHeaderPayloadEncryptionKeyAESBlock := common.Must2(aes.NewCipher(aeadResponseHeaderPayloadEncryptionKey)).(cipher.Block)
|
||||
aeadResponseHeaderPayloadEncryptionAEAD := common.Must2(cipher.NewGCM(aeadResponseHeaderPayloadEncryptionKeyAESBlock)).(cipher.AEAD)
|
||||
|
||||
aeadEncryptedHeaderPayload := aeadResponseHeaderPayloadEncryptionAEAD.Seal(nil, aeadResponseHeaderPayloadEncryptionIV, aeadEncryptedHeaderBuffer.Bytes(), nil)
|
||||
common.Must2(io.Copy(writer, bytes.NewReader(aeadEncryptedHeaderPayload)))
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeResponseBody returns a Writer that auto-encrypt content written by caller.
|
||||
func (s *ServerSession) EncodeResponseBody(request *protocol.RequestHeader, writer io.Writer) buf.Writer {
|
||||
var sizeParser crypto.ChunkSizeEncoder = crypto.PlainChunkSizeParser{}
|
||||
if request.Option.Has(protocol.RequestOptionChunkMasking) {
|
||||
sizeParser = NewShakeSizeParser(s.responseBodyIV[:])
|
||||
}
|
||||
var padding crypto.PaddingLengthGenerator
|
||||
if request.Option.Has(protocol.RequestOptionGlobalPadding) {
|
||||
padding = sizeParser.(crypto.PaddingLengthGenerator)
|
||||
}
|
||||
|
||||
switch request.Security {
|
||||
case protocol.SecurityType_NONE:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
if request.Command.TransferType() == protocol.TransferTypeStream {
|
||||
return crypto.NewChunkStreamWriter(sizeParser, writer)
|
||||
}
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(NoOpAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding)
|
||||
}
|
||||
return buf.NewWriter(writer)
|
||||
|
||||
case protocol.SecurityType_LEGACY:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(FnvAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, s.responseWriter, request.Command.TransferType(), padding)
|
||||
}
|
||||
return &buf.SequentialWriter{Writer: s.responseWriter}
|
||||
|
||||
case protocol.SecurityType_AES128_GCM:
|
||||
aead := crypto.NewAesGcm(s.responseBodyKey[:])
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(s.responseBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding)
|
||||
|
||||
case protocol.SecurityType_CHACHA20_POLY1305:
|
||||
aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(s.responseBodyKey[:]))
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: aead,
|
||||
NonceGenerator: GenerateChunkNonce(s.responseBodyIV[:], uint32(aead.NonceSize())),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding)
|
||||
|
||||
default:
|
||||
panic("Unknown security type.")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerSession) DrainConnN(reader io.Reader, n int) error {
|
||||
_, err := io.CopyN(ioutil.Discard, reader, int64(n))
|
||||
return err
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue