diff --git a/proxy/vmess/auth.go b/proxy/vmess/auth.go new file mode 100644 index 0000000..9228ffe --- /dev/null +++ b/proxy/vmess/auth.go @@ -0,0 +1,136 @@ +package vmess + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/binary" + "hash" + "hash/crc32" + "io" + "time" +) + +// https://github.com/v2fly/v2ray-core/tree/327cbdc5cd10ee1aec6ef4dce1af027b3e277f33/proxy/vmess/aead +const ( + kdfSaltConstAuthIDEncryptionKey = "AES Auth ID Encryption" + kdfSaltConstAEADRespHeaderLenKey = "AEAD Resp Header Len Key" + kdfSaltConstAEADRespHeaderLenIV = "AEAD Resp Header Len IV" + kdfSaltConstAEADRespHeaderPayloadKey = "AEAD Resp Header Key" + kdfSaltConstAEADRespHeaderPayloadIV = "AEAD Resp Header IV" + kdfSaltConstVMessAEADKDF = "VMess AEAD KDF" + kdfSaltConstVMessHeaderPayloadAEADKey = "VMess Header AEAD Key" + kdfSaltConstVMessHeaderPayloadAEADIV = "VMess Header AEAD Nonce" + kdfSaltConstVMessHeaderPayloadLengthAEADKey = "VMess Header AEAD Key_Length" + kdfSaltConstVMessHeaderPayloadLengthAEADIV = "VMess Header AEAD Nonce_Length" +) + +func sealVMessAEADHeader(key [16]byte, data []byte) []byte { + generatedAuthID := createAuthID(key[:], time.Now().Unix()) + connectionNonce := make([]byte, 8) + rand.Read(connectionNonce) + + aeadPayloadLengthSerializedByte := make([]byte, 2) + binary.BigEndian.PutUint16(aeadPayloadLengthSerializedByte, uint16(len(data))) + + var payloadHeaderLengthAEADEncrypted []byte + + { + payloadHeaderLengthAEADKey := kdf(key[:], kdfSaltConstVMessHeaderPayloadLengthAEADKey, string(generatedAuthID[:]), string(connectionNonce))[:16] + payloadHeaderLengthAEADNonce := kdf(key[:], kdfSaltConstVMessHeaderPayloadLengthAEADIV, string(generatedAuthID[:]), string(connectionNonce))[:12] + payloadHeaderLengthAEADAESBlock, _ := aes.NewCipher(payloadHeaderLengthAEADKey) + payloadHeaderAEAD, _ := cipher.NewGCM(payloadHeaderLengthAEADAESBlock) + payloadHeaderLengthAEADEncrypted = payloadHeaderAEAD.Seal(nil, payloadHeaderLengthAEADNonce, aeadPayloadLengthSerializedByte, generatedAuthID[:]) + } + + var payloadHeaderAEADEncrypted []byte + + { + payloadHeaderAEADKey := kdf(key[:], kdfSaltConstVMessHeaderPayloadAEADKey, string(generatedAuthID[:]), string(connectionNonce))[:16] + payloadHeaderAEADNonce := kdf(key[:], kdfSaltConstVMessHeaderPayloadAEADIV, string(generatedAuthID[:]), string(connectionNonce))[:12] + payloadHeaderAEADAESBlock, _ := aes.NewCipher(payloadHeaderAEADKey) + payloadHeaderAEAD, _ := cipher.NewGCM(payloadHeaderAEADAESBlock) + payloadHeaderAEADEncrypted = payloadHeaderAEAD.Seal(nil, payloadHeaderAEADNonce, data, generatedAuthID[:]) + } + + var outputBuffer = &bytes.Buffer{} + + outputBuffer.Write(generatedAuthID[:]) + outputBuffer.Write(payloadHeaderLengthAEADEncrypted) + outputBuffer.Write(connectionNonce) + outputBuffer.Write(payloadHeaderAEADEncrypted) + + return outputBuffer.Bytes() +} + +func openVMessAEADHeader(key [16]byte, iv [16]byte, r io.Reader) ([]byte, error) { + aeadResponseHeaderLengthEncryptionKey := kdf(key[:], kdfSaltConstAEADRespHeaderLenKey)[:16] + aeadResponseHeaderLengthEncryptionIV := kdf(iv[:], kdfSaltConstAEADRespHeaderLenIV)[:12] + + aeadResponseHeaderLengthEncryptionKeyAESBlock, _ := aes.NewCipher(aeadResponseHeaderLengthEncryptionKey) + aeadResponseHeaderLengthEncryptionAEAD, _ := cipher.NewGCM(aeadResponseHeaderLengthEncryptionKeyAESBlock) + + aeadEncryptedResponseHeaderLength := make([]byte, 18) + if _, err := io.ReadFull(r, aeadEncryptedResponseHeaderLength); err != nil { + return nil, err + } + + decryptedResponseHeaderLengthBinaryBuffer, err := aeadResponseHeaderLengthEncryptionAEAD.Open(nil, aeadResponseHeaderLengthEncryptionIV, aeadEncryptedResponseHeaderLength[:], nil) + if err != nil { + return nil, err + } + + decryptedResponseHeaderLength := binary.BigEndian.Uint16(decryptedResponseHeaderLengthBinaryBuffer) + aeadResponseHeaderPayloadEncryptionKey := kdf(key[:], kdfSaltConstAEADRespHeaderPayloadKey)[:16] + aeadResponseHeaderPayloadEncryptionIV := kdf(iv[:], kdfSaltConstAEADRespHeaderPayloadIV)[:12] + aeadResponseHeaderPayloadEncryptionKeyAESBlock, _ := aes.NewCipher(aeadResponseHeaderPayloadEncryptionKey) + aeadResponseHeaderPayloadEncryptionAEAD, _ := cipher.NewGCM(aeadResponseHeaderPayloadEncryptionKeyAESBlock) + + encryptedResponseHeaderBuffer := make([]byte, decryptedResponseHeaderLength+16) + if _, err := io.ReadFull(r, encryptedResponseHeaderBuffer); err != nil { + return nil, err + } + + return aeadResponseHeaderPayloadEncryptionAEAD.Open(nil, aeadResponseHeaderPayloadEncryptionIV, encryptedResponseHeaderBuffer, nil) +} + +func kdf(key []byte, path ...string) []byte { + hmacCreator := &hMacCreator{value: []byte(kdfSaltConstVMessAEADKDF)} + for _, v := range path { + hmacCreator = &hMacCreator{value: []byte(v), parent: hmacCreator} + } + hmacf := hmacCreator.Create() + hmacf.Write(key) + return hmacf.Sum(nil) +} + +type hMacCreator struct { + parent *hMacCreator + value []byte +} + +func (h *hMacCreator) Create() hash.Hash { + if h.parent == nil { + return hmac.New(sha256.New, h.value) + } + return hmac.New(h.parent.Create, h.value) +} + +func createAuthID(cmdKey []byte, time int64) [16]byte { + buf := &bytes.Buffer{} + binary.Write(buf, binary.BigEndian, time) + + random := make([]byte, 4) + rand.Read(random) + buf.Write(random) + zero := crc32.ChecksumIEEE(buf.Bytes()) + binary.Write(buf, binary.BigEndian, zero) + + aesBlock, _ := aes.NewCipher(kdf(cmdKey[:], kdfSaltConstAuthIDEncryptionKey)[:16]) + var result [16]byte + aesBlock.Encrypt(result[:], buf.Bytes()) + return result +} diff --git a/proxy/vmess/client.go b/proxy/vmess/client.go index 7c840e3..07a35e0 100644 --- a/proxy/vmess/client.go +++ b/proxy/vmess/client.go @@ -5,6 +5,7 @@ import ( "crypto/cipher" "crypto/hmac" "crypto/md5" + "crypto/sha256" "encoding/binary" "errors" "hash/fnv" @@ -17,6 +18,7 @@ import ( "golang.org/x/crypto/chacha20poly1305" + "github.com/nadoo/glider/log" "github.com/nadoo/glider/pool" ) @@ -49,6 +51,7 @@ type Client struct { users []*User count int opt byte + aead bool security byte } @@ -56,6 +59,7 @@ type Client struct { type Conn struct { user *User opt byte + aead bool security byte atyp Atyp @@ -74,7 +78,7 @@ type Conn struct { } // NewClient returns a new vmess client. -func NewClient(uuidStr, security string, alterID int) (*Client, error) { +func NewClient(uuidStr, security string, alterID int, aead bool) (*Client, error) { uuid, err := StrToUUID(uuidStr) if err != nil { return nil, err @@ -87,6 +91,7 @@ func NewClient(uuidStr, security string, alterID int) (*Client, error) { c.count = len(c.users) c.opt = OptChunkStream + c.aead = aead security = strings.ToLower(security) switch security { @@ -114,7 +119,7 @@ func NewClient(uuidStr, security string, alterID int) (*Client, error) { // NewConn returns a new vmess conn. func (c *Client) NewConn(rc net.Conn, target string, cmd CmdType) (*Conn, error) { r := rand.Intn(c.count) - conn := &Conn{user: c.users[r], opt: c.opt, security: c.security, Conn: rc} + conn := &Conn{user: c.users[r], opt: c.opt, aead: c.aead, security: c.security, Conn: rc} var err error conn.atyp, conn.addr, conn.port, err = ParseAddr(target) @@ -130,13 +135,20 @@ func (c *Client) NewConn(rc net.Conn, target string, cmd CmdType) (*Conn, error) conn.reqRespV = byte(rand.Intn(1 << 8)) - conn.respBodyIV = md5.Sum(conn.reqBodyIV[:]) - conn.respBodyKey = md5.Sum(conn.reqBodyKey[:]) + if conn.aead { + bodyIV := sha256.Sum256(conn.reqBodyIV[:]) + bodyKey := sha256.Sum256(conn.reqBodyKey[:]) + copy(conn.respBodyIV[:], bodyIV[:16]) + copy(conn.respBodyKey[:], bodyKey[:16]) + } else { + conn.respBodyIV = md5.Sum(conn.reqBodyIV[:]) + conn.respBodyKey = md5.Sum(conn.reqBodyKey[:]) - // Auth - err = conn.Auth() - if err != nil { - return nil, err + // MD5 Auth + err = conn.Auth() + if err != nil { + return nil, err + } } // Request @@ -183,13 +195,9 @@ func (c *Conn) Request(cmd CmdType) error { buf.WriteByte(byte(cmd)) // cmd // target - err := binary.Write(buf, binary.BigEndian, uint16(c.port)) // port - if err != nil { - return err - } - - buf.WriteByte(byte(c.atyp)) // atyp - buf.Write(c.addr) // addr + binary.Write(buf, binary.BigEndian, uint16(c.port)) // port + buf.WriteByte(byte(c.atyp)) // atyp + buf.Write(c.addr) // addr // padding if paddingLen > 0 { @@ -201,11 +209,14 @@ func (c *Conn) Request(cmd CmdType) error { // F fnv1a := fnv.New32a() - _, err = fnv1a.Write(buf.Bytes()) - if err != nil { + fnv1a.Write(buf.Bytes()) + buf.Write(fnv1a.Sum(nil)) + + if c.aead { + encHeader := sealVMessAEADHeader(c.user.CmdKey, buf.Bytes()) + _, err := c.Conn.Write(encHeader) return err } - buf.Write(fnv1a.Sum(nil)) block, err := aes.NewCipher(c.user.CmdKey[:]) if err != nil { @@ -222,29 +233,49 @@ func (c *Conn) Request(cmd CmdType) error { // DecodeRespHeader decodes response header. func (c *Conn) DecodeRespHeader() error { + if c.aead { + buf, err := openVMessAEADHeader(c.respBodyKey, c.respBodyIV, c.Conn) + if err != nil { + return err + } + + if len(buf) < 4 { + return errors.New("unexpected buffer length") + } + + if buf[0] != c.reqRespV { + return errors.New("unexpected response header") + } + + // TODO: Dynamic port support + if buf[2] != 0 { + // dataLen := int32(buf[3]) + return errors.New("dynamic port is not supported now") + } + return nil + } + block, err := aes.NewCipher(c.respBodyKey[:]) if err != nil { return err } - stream := cipher.NewCFBDecrypter(block, c.respBodyIV[:]) - b := pool.GetBuffer(4) - defer pool.PutBuffer(b) + buf := pool.GetBuffer(4) + defer pool.PutBuffer(buf) - _, err = io.ReadFull(c.Conn, b) + _, err = io.ReadFull(c.Conn, buf) if err != nil { return err } + stream.XORKeyStream(buf, buf) - stream.XORKeyStream(b, b) - - if b[0] != c.reqRespV { + if buf[0] != c.reqRespV { return errors.New("unexpected response header") } // TODO: Dynamic port support - if b[2] != 0 { + if buf[2] != 0 { // dataLen := int32(buf[3]) return errors.New("dynamic port is not supported now") } @@ -290,6 +321,7 @@ func (c *Conn) Read(b []byte) (n int, err error) { err = c.DecodeRespHeader() if err != nil { + log.F("[vmess] DecodeRespHeader error: %s", err) return 0, err } diff --git a/proxy/vmess/vmess.go b/proxy/vmess/vmess.go index 708382b..929cdde 100644 --- a/proxy/vmess/vmess.go +++ b/proxy/vmess/vmess.go @@ -15,6 +15,7 @@ type VMess struct { addr string uuid string + aead bool alterID int security string @@ -45,7 +46,7 @@ func NewVMess(s string, d proxy.Dialer) (*VMess, error) { query := u.Query() aid := query.Get("alterID") if aid == "" { - aid = "1" + aid = "0" } alterID, err := strconv.ParseUint(aid, 10, 32) @@ -54,7 +55,8 @@ func NewVMess(s string, d proxy.Dialer) (*VMess, error) { return nil, err } - client, err := NewClient(uuid, security, int(alterID)) + aead := alterID == 0 + client, err := NewClient(uuid, security, int(alterID), aead) if err != nil { log.F("create vmess client err: %s", err) return nil, err