Feature: add vmess support

This commit is contained in:
Dreamacro
2018-09-06 10:53:29 +08:00
parent af13acc171
commit 834baa9e27
10 changed files with 772 additions and 16 deletions

115
common/vmess/aead.go Normal file
View File

@ -0,0 +1,115 @@
package vmess
import (
"crypto/cipher"
"encoding/binary"
"errors"
"io"
)
type aeadWriter struct {
io.Writer
cipher.AEAD
nonce [32]byte
count uint16
iv []byte
}
func newAEADWriter(w io.Writer, aead cipher.AEAD, iv []byte) *aeadWriter {
return &aeadWriter{Writer: w, AEAD: aead, iv: iv}
}
func (w *aeadWriter) Write(b []byte) (n int, err error) {
buf := bufPool.Get().([]byte)
defer bufPool.Put(buf[:cap(buf)])
length := len(b)
for {
if length == 0 {
break
}
readLen := chunkSize - w.Overhead()
if length < readLen {
readLen = length
}
payloadBuf := buf[lenSize : lenSize+chunkSize-w.Overhead()]
copy(payloadBuf, b[n:n+readLen])
binary.BigEndian.PutUint16(buf[:lenSize], uint16(readLen+w.Overhead()))
binary.BigEndian.PutUint16(w.nonce[:2], w.count)
copy(w.nonce[2:], w.iv[2:12])
w.Seal(payloadBuf[:0], w.nonce[:w.NonceSize()], payloadBuf[:readLen], nil)
w.count++
_, err = w.Writer.Write(buf[:lenSize+readLen+w.Overhead()])
if err != nil {
break
}
n += readLen
length -= readLen
}
return
}
type aeadReader struct {
io.Reader
cipher.AEAD
nonce [32]byte
buf []byte
offset int
iv []byte
sizeBuf []byte
count uint16
}
func newAEADReader(r io.Reader, aead cipher.AEAD, iv []byte) *aeadReader {
return &aeadReader{Reader: r, AEAD: aead, iv: iv, sizeBuf: make([]byte, lenSize)}
}
func (r *aeadReader) Read(b []byte) (int, error) {
if r.buf != nil {
n := copy(b, r.buf[r.offset:])
r.offset += n
if r.offset == len(r.buf) {
bufPool.Put(r.buf[:cap(r.buf)])
r.buf = nil
}
return n, nil
}
_, err := io.ReadFull(r.Reader, r.sizeBuf)
if err != nil {
return 0, err
}
size := int(binary.BigEndian.Uint16(r.sizeBuf))
if size > maxSize {
return 0, errors.New("Buffer is larger than standard")
}
buf := bufPool.Get().([]byte)
_, err = io.ReadFull(r.Reader, buf[:size])
if err != nil {
bufPool.Put(buf[:cap(buf)])
return 0, err
}
binary.BigEndian.PutUint16(r.nonce[:2], r.count)
copy(r.nonce[2:], r.iv[2:12])
_, err = r.Open(buf[:0], r.nonce[:r.NonceSize()], buf[:size], nil)
r.count++
if err != nil {
return 0, err
}
realLen := size - r.Overhead()
n := copy(b, buf[:realLen])
if len(b) >= realLen {
bufPool.Put(buf[:cap(buf)])
return n, nil
}
r.offset = n
r.buf = buf[:realLen]
return n, nil
}

103
common/vmess/chunk.go Normal file
View File

@ -0,0 +1,103 @@
package vmess
import (
"encoding/binary"
"errors"
"io"
"sync"
)
const (
lenSize = 2
chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024
maxSize = 17 * 1024 // 2 + chunkSize + aead.Overhead()
)
var bufPool = sync.Pool{New: func() interface{} { return make([]byte, maxSize) }}
type chunkReader struct {
io.Reader
buf []byte
sizeBuf []byte
offset int
}
func newChunkReader(reader io.Reader) *chunkReader {
return &chunkReader{Reader: reader, sizeBuf: make([]byte, lenSize)}
}
func newChunkWriter(writer io.WriteCloser) *chunkWriter {
return &chunkWriter{Writer: writer}
}
func (cr *chunkReader) Read(b []byte) (int, error) {
if cr.buf != nil {
n := copy(b, cr.buf[cr.offset:])
cr.offset += n
if cr.offset == len(cr.buf) {
bufPool.Put(cr.buf[:cap(cr.buf)])
cr.buf = nil
}
return n, nil
}
_, err := io.ReadFull(cr.Reader, cr.sizeBuf)
if err != nil {
return 0, err
}
size := int(binary.BigEndian.Uint16(cr.sizeBuf))
if size > maxSize {
return 0, errors.New("Buffer is larger than standard")
}
if len(b) >= size {
_, err := io.ReadFull(cr.Reader, b[:size])
if err != nil {
return 0, err
}
return size, nil
}
buf := bufPool.Get().([]byte)
_, err = io.ReadFull(cr.Reader, buf[:size])
if err != nil {
bufPool.Put(buf[:cap(buf)])
return 0, err
}
n := copy(b, cr.buf[:])
cr.offset = n
cr.buf = buf[:size]
return n, nil
}
type chunkWriter struct {
io.Writer
}
func (cw *chunkWriter) Write(b []byte) (n int, err error) {
buf := bufPool.Get().([]byte)
defer bufPool.Put(buf[:cap(buf)])
length := len(b)
for {
if length == 0 {
break
}
readLen := chunkSize
if length < chunkSize {
readLen = length
}
payloadBuf := buf[lenSize : lenSize+chunkSize]
copy(payloadBuf, b[n:n+readLen])
binary.BigEndian.PutUint16(buf[:lenSize], uint16(readLen))
_, err = cw.Writer.Write(buf[:lenSize+readLen])
if err != nil {
break
}
n += readLen
length -= readLen
}
return
}

213
common/vmess/conn.go Normal file
View File

@ -0,0 +1,213 @@
package vmess
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/md5"
"encoding/binary"
"errors"
"hash/fnv"
"io"
"math/rand"
"net"
"time"
"golang.org/x/crypto/chacha20poly1305"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
// Conn wrapper a net.Conn with vmess protocol
type Conn struct {
net.Conn
reader io.Reader
writer io.Writer
dst *DstAddr
id *ID
reqBodyIV []byte
reqBodyKey []byte
respBodyIV []byte
respBodyKey []byte
respV byte
security byte
sent bool
received bool
}
func (vc *Conn) Write(b []byte) (int, error) {
if vc.sent {
return vc.writer.Write(b)
}
if err := vc.sendRequest(); err != nil {
return 0, err
}
vc.sent = true
return vc.writer.Write(b)
}
func (vc *Conn) Read(b []byte) (int, error) {
if vc.received {
return vc.reader.Read(b)
}
if err := vc.recvResponse(); err != nil {
return 0, err
}
vc.received = true
return vc.reader.Read(b)
}
func (vc *Conn) sendRequest() error {
timestamp := make([]byte, 8)
binary.BigEndian.PutUint64(timestamp, uint64(time.Now().UTC().Unix()))
h := hmac.New(md5.New, vc.id.UUID.Bytes())
h.Write(timestamp)
_, err := vc.Conn.Write(h.Sum(nil))
if err != nil {
return err
}
buf := &bytes.Buffer{}
// Ver IV Key V Opt
buf.WriteByte(Version)
buf.Write(vc.reqBodyIV[:])
buf.Write(vc.reqBodyKey[:])
buf.WriteByte(vc.respV)
buf.WriteByte(OptionChunkStream)
p := rand.Intn(16)
// P Sec Reserve Cmd
buf.WriteByte(byte(p<<4) | byte(vc.security))
buf.WriteByte(0)
buf.WriteByte(CommandTCP)
// Port AddrType Addr
binary.Write(buf, binary.BigEndian, uint16(vc.dst.Port))
buf.WriteByte(vc.dst.AddrType)
buf.Write(vc.dst.Addr)
// padding
if p > 0 {
padding := make([]byte, p)
rand.Read(padding)
buf.Write(padding)
}
fnv1a := fnv.New32a()
fnv1a.Write(buf.Bytes())
buf.Write(fnv1a.Sum(nil))
block, err := aes.NewCipher(vc.id.CmdKey)
if err != nil {
return err
}
stream := cipher.NewCFBEncrypter(block, hashTimestamp(time.Now().UTC()))
stream.XORKeyStream(buf.Bytes(), buf.Bytes())
_, err = vc.Conn.Write(buf.Bytes())
return err
}
func (vc *Conn) recvResponse() error {
block, err := aes.NewCipher(vc.respBodyKey[:])
if err != nil {
return err
}
stream := cipher.NewCFBDecrypter(block, vc.respBodyIV[:])
buf := make([]byte, 4)
_, err = io.ReadFull(vc.Conn, buf)
if err != nil {
return err
}
stream.XORKeyStream(buf, buf)
if buf[0] != vc.respV {
return errors.New("unexpected response header")
}
if buf[2] != 0 {
return errors.New("dynamic port is not supported now")
}
return nil
}
func hashTimestamp(t time.Time) []byte {
md5hash := md5.New()
ts := make([]byte, 8)
binary.BigEndian.PutUint64(ts, uint64(t.UTC().Unix()))
md5hash.Write(ts)
md5hash.Write(ts)
md5hash.Write(ts)
md5hash.Write(ts)
return md5hash.Sum(nil)
}
// newConn return a Conn instance
func newConn(conn net.Conn, id *ID, dst *DstAddr, security Security) *Conn {
randBytes := make([]byte, 33)
rand.Read(randBytes)
reqBodyIV := make([]byte, 16)
reqBodyKey := make([]byte, 16)
copy(reqBodyIV[:], randBytes[:16])
copy(reqBodyKey[:], randBytes[16:32])
respV := randBytes[32]
respBodyKey := md5.Sum(reqBodyKey[:])
respBodyIV := md5.Sum(reqBodyIV[:])
var writer io.Writer
var reader io.Reader
switch security {
case SecurityNone:
reader = newChunkReader(conn)
writer = newChunkWriter(conn)
case SecurityAES128GCM:
block, _ := aes.NewCipher(reqBodyKey[:])
aead, _ := cipher.NewGCM(block)
writer = newAEADWriter(conn, aead, reqBodyIV[:])
block, _ = aes.NewCipher(respBodyKey[:])
aead, _ = cipher.NewGCM(block)
reader = newAEADReader(conn, aead, respBodyIV[:])
case SecurityCHACHA20POLY1305:
key := make([]byte, 32)
t := md5.Sum(reqBodyKey[:])
copy(key, t[:])
t = md5.Sum(key[:16])
copy(key[16:], t[:])
aead, _ := chacha20poly1305.New(key)
writer = newAEADWriter(conn, aead, reqBodyIV[:])
t = md5.Sum(respBodyKey[:])
copy(key, t[:])
t = md5.Sum(key[:16])
copy(key[16:], t[:])
aead, _ = chacha20poly1305.New(key)
reader = newAEADReader(conn, aead, respBodyIV[:])
}
return &Conn{
Conn: conn,
id: id,
dst: dst,
reqBodyIV: reqBodyIV,
reqBodyKey: reqBodyKey,
respV: respV,
respBodyIV: respBodyIV[:],
respBodyKey: respBodyKey[:],
reader: reader,
writer: writer,
security: security,
}
}

54
common/vmess/user.go Normal file
View File

@ -0,0 +1,54 @@
package vmess
import (
"bytes"
"crypto/md5"
"github.com/gofrs/uuid"
)
// ID cmdKey length
const (
IDBytesLen = 16
)
// The ID of en entity, in the form of a UUID.
type ID struct {
UUID *uuid.UUID
CmdKey []byte
}
// newID returns an ID with given UUID.
func newID(uuid *uuid.UUID) *ID {
id := &ID{UUID: uuid, CmdKey: make([]byte, IDBytesLen)}
md5hash := md5.New()
md5hash.Write(uuid.Bytes())
md5hash.Write([]byte("c48619fe-8f02-49e0-b9e9-edf763e17e21"))
md5hash.Sum(id.CmdKey[:0])
return id
}
func nextID(u *uuid.UUID) *uuid.UUID {
md5hash := md5.New()
md5hash.Write(u.Bytes())
md5hash.Write([]byte("16167dc8-16b6-4e6d-b8bb-65dd68113a81"))
var newid uuid.UUID
for {
md5hash.Sum(newid[:0])
if !bytes.Equal(newid.Bytes(), u.Bytes()) {
return &newid
}
md5hash.Write([]byte("533eff8a-4113-4b10-b5ce-0f5d76b98cd2"))
}
}
func newAlterIDs(primary *ID, alterIDCount uint16) []*ID {
alterIDs := make([]*ID, alterIDCount)
prevID := primary.UUID
for idx := range alterIDs {
newid := nextID(prevID)
alterIDs[idx] = &ID{UUID: newid, CmdKey: primary.CmdKey[:]}
prevID = newid
}
return alterIDs
}

106
common/vmess/vmess.go Normal file
View File

@ -0,0 +1,106 @@
package vmess
import (
"fmt"
"math/rand"
"net"
"runtime"
"github.com/gofrs/uuid"
)
// Version of vmess
const Version byte = 1
// Request Options
const (
OptionChunkStream byte = 1
OptionChunkMasking byte = 4
)
// Security type vmess
type Security = byte
// Cipher types
const (
SecurityAES128GCM Security = 3
SecurityCHACHA20POLY1305 Security = 4
SecurityNone Security = 5
)
// CipherMapping return
var CipherMapping = map[string]byte{
"none": SecurityNone,
"aes-128-gcm": SecurityAES128GCM,
"chacha20-poly1305": SecurityCHACHA20POLY1305,
}
// Command types
const (
CommandTCP byte = 1
CommandUDP byte = 2
)
// Addr types
const (
AtypIPv4 byte = 1
AtypDomainName byte = 2
AtypIPv6 byte = 3
)
// DstAddr store destination address
type DstAddr struct {
AddrType byte
Addr []byte
Port uint
}
// Client is vmess connection generator
type Client struct {
user []*ID
uuid *uuid.UUID
security Security
}
// Config of vmess
type Config struct {
UUID string
AlterID uint16
Security string
}
// New return a Conn with net.Conn and DstAddr
func (c *Client) New(conn net.Conn, dst *DstAddr) net.Conn {
r := rand.Intn(len(c.user))
return newConn(conn, c.user[r], dst, c.security)
}
// NewClient return Client instance
func NewClient(config Config) (*Client, error) {
uid, err := uuid.FromString(config.UUID)
if err != nil {
return nil, err
}
var security Security
switch config.Security {
case "aes-128-gcm":
security = SecurityAES128GCM
case "chacha20-poly1305":
security = SecurityCHACHA20POLY1305
case "none":
security = SecurityNone
case "auto":
security = SecurityCHACHA20POLY1305
if runtime.GOARCH == "amd64" || runtime.GOARCH == "s390x" || runtime.GOARCH == "arm64" {
security = SecurityAES128GCM
}
default:
return nil, fmt.Errorf("Unknown security type: %s", config.Security)
}
return &Client{
user: newAlterIDs(newID(&uid), config.AlterID),
uuid: &uid,
security: security,
}, nil
}