From 834baa9e270f01dfd8337d0b1ffd160602e8dcf9 Mon Sep 17 00:00:00 2001 From: Dreamacro <305009791@qq.com> Date: Thu, 6 Sep 2018 10:53:29 +0800 Subject: [PATCH] =?UTF-8?q?Feature:=20=E2=9C=A8=20add=20vmess=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gopkg.lock | 70 ++++++++++--- Gopkg.toml | 16 ++- adapters/remote/vmess.go | 93 +++++++++++++++++ common/vmess/aead.go | 115 +++++++++++++++++++++ common/vmess/chunk.go | 103 +++++++++++++++++++ common/vmess/conn.go | 213 +++++++++++++++++++++++++++++++++++++++ common/vmess/user.go | 54 ++++++++++ common/vmess/vmess.go | 106 +++++++++++++++++++ config/config.go | 15 +++ constant/adapters.go | 3 + 10 files changed, 772 insertions(+), 16 deletions(-) create mode 100644 adapters/remote/vmess.go create mode 100644 common/vmess/aead.go create mode 100644 common/vmess/chunk.go create mode 100644 common/vmess/conn.go create mode 100644 common/vmess/user.go create mode 100644 common/vmess/vmess.go diff --git a/Gopkg.lock b/Gopkg.lock index 4dab52be..638a039a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -3,65 +3,92 @@ [[projects]] branch = "master" + digest = "1:8fa55a6e302771a90a86ceae1ca3c0df4ef15d21092198e8313f61dde9eea963" name = "github.com/Yawning/chacha20" packages = ["."] + pruneopts = "UT" revision = "e3b1f968fc6397b51d963fee8ec8711a47bc0ce8" [[projects]] + digest = "1:444b82bfe35c83bbcaf84e310fb81a1f9ece03edfed586483c869e2c046aef69" name = "github.com/eapache/queue" packages = ["."] + pruneopts = "UT" revision = "44cc805cf13205b55f69e14bcb69867d1ae92f98" version = "v1.1.0" [[projects]] + digest = "1:b9914f85d95a0968bafd1be1908ba29e2eafafd88d6fd13696be42bf5368c380" name = "github.com/go-chi/chi" packages = ["."] - revision = "e83ac2304db3c50cf03d96a2fcd39009d458bc35" - version = "v3.3.2" + pruneopts = "UT" + revision = "b5294d10673813fac8558e7f47242bc9e61b4c25" + version = "v3.3.3" [[projects]] + digest = "1:dfa416a1bb8139f30832543340f972f65c0db9932034cb6a1b42c5ac615a3fb8" name = "github.com/go-chi/cors" packages = ["."] + pruneopts = "UT" revision = "dba6525398619dead495962a916728e7ee2ca322" version = "v1.0.0" [[projects]] + digest = "1:54d7b4b9ab2bb2bae35b55eea900bc8fe15cba05a95fc78bf7fd7b82a9a07afa" name = "github.com/go-chi/render" packages = ["."] + pruneopts = "UT" revision = "3215478343fbc559bd3fc08f7031bb134d6bdad5" version = "v1.0.1" [[projects]] + digest = "1:ce579162ae1341f3e5ab30c0dce767f28b1eb6a81359aad01723f1ba6b4becdf" + name = "github.com/gofrs/uuid" + packages = ["."] + pruneopts = "UT" + revision = "370558f003bfe29580cd0f698d8640daccdcc45c" + version = "v3.1.1" + +[[projects]] + digest = "1:2e8bdbc8a11d716dab1bf66d326285d7e5c92fa4c996c1574ba1153e57534b85" name = "github.com/oschwald/geoip2-golang" packages = ["."] + pruneopts = "UT" revision = "7118115686e16b77967cdbf55d1b944fe14ad312" version = "v1.2.1" [[projects]] + digest = "1:07e8589503b7ec22430dae6eed6f2c17e4249ab245574f4fd0ba8fc9c597d138" name = "github.com/oschwald/maxminddb-golang" packages = ["."] + pruneopts = "UT" revision = "c5bec84d1963260297932a1b7a1753c8420717a7" version = "v1.3.0" [[projects]] + digest = "1:2bfa1a73654feb90893014b04779ce5205f3e19e843c0e32a89ea051d31f12d5" name = "github.com/riobard/go-shadowsocks2" packages = [ "core", "shadowaead", "shadowstream", - "socks" + "socks", ] + pruneopts = "UT" revision = "8346403248229fc7e10d7a259de8e9352a9d8830" version = "v0.1.0" [[projects]] + digest = "1:d867dfa6751c8d7a435821ad3b736310c2ed68945d05b50fb9d23aee0540c8cc" name = "github.com/sirupsen/logrus" packages = ["."] - revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" - version = "v1.0.5" + pruneopts = "UT" + revision = "3e01752db0189b9157070a0e1668a620f9a85da2" + version = "v1.0.6" [[projects]] branch = "master" + digest = "1:d33888518d56c3f0cc9009594f56be4faf33ffff358fe10ff8e7e8cccf0e6617" name = "golang.org/x/crypto" packages = [ "chacha20poly1305", @@ -69,35 +96,54 @@ "internal/chacha20", "internal/subtle", "poly1305", - "ssh/terminal" + "ssh/terminal", ] - revision = "a2144134853fc9a27a7b1e3eb4f19f1a76df13c9" + pruneopts = "UT" + revision = "0709b304e793a5edb4a2c0145f281ecdc20838a4" [[projects]] branch = "master" + digest = "1:576f8d82185dc836ec6d10c0e5568dc4ff94e4d9f101d33ed5d6bae0cbba65b2" name = "golang.org/x/sys" packages = [ "cpu", "unix", - "windows" + "windows", ] - revision = "ac767d655b305d4e9612f5f6e33120b9176c4ad4" + pruneopts = "UT" + revision = "ebe1bf3edb3325c393447059974de898d5133eb8" [[projects]] + digest = "1:975a4480c40f2d0b95e1f83d3ec1aa29a2774e80179e08a9a4ba2aab86721b23" name = "gopkg.in/eapache/channels.v1" packages = ["."] + pruneopts = "UT" revision = "47238d5aae8c0fefd518ef2bee46290909cf8263" version = "v1.1.0" [[projects]] + digest = "1:5abd6a22805b1919f6a6bca0ae58b13cef1f3412812f38569978f43ef02743d4" name = "gopkg.in/ini.v1" packages = ["."] - revision = "358ee7663966325963d4e8b2e1fbd570c5195153" - version = "v1.38.1" + pruneopts = "UT" + revision = "5cf292cae48347c2490ac1a58fe36735fb78df7e" + version = "v1.38.2" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "0ccb06b3617c87e75bd650f92adc99e55b93070e0b2a0bc71634270226e125fc" + input-imports = [ + "github.com/go-chi/chi", + "github.com/go-chi/cors", + "github.com/go-chi/render", + "github.com/gofrs/uuid", + "github.com/oschwald/geoip2-golang", + "github.com/riobard/go-shadowsocks2/core", + "github.com/riobard/go-shadowsocks2/socks", + "github.com/sirupsen/logrus", + "golang.org/x/crypto/chacha20poly1305", + "gopkg.in/eapache/channels.v1", + "gopkg.in/ini.v1", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index c381b4d9..9b9d252d 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -1,6 +1,6 @@ # Gopkg.toml example # -# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html # for detailed Gopkg.toml documentation. # # required = ["github.com/user/thing/cmd/thing"] @@ -27,7 +27,7 @@ [[constraint]] name = "github.com/go-chi/chi" - version = "3.3.2" + version = "3.3.3" [[constraint]] name = "github.com/go-chi/cors" @@ -37,6 +37,10 @@ name = "github.com/go-chi/render" version = "1.0.1" +[[constraint]] + name = "github.com/gofrs/uuid" + version = "3.1.1" + [[constraint]] name = "github.com/oschwald/geoip2-golang" version = "1.2.1" @@ -47,7 +51,11 @@ [[constraint]] name = "github.com/sirupsen/logrus" - version = "1.0.5" + version = "1.0.6" + +[[constraint]] + branch = "master" + name = "golang.org/x/crypto" [[constraint]] name = "gopkg.in/eapache/channels.v1" @@ -55,7 +63,7 @@ [[constraint]] name = "gopkg.in/ini.v1" - version = "1.38.1" + version = "1.38.2" [prune] go-tests = true diff --git a/adapters/remote/vmess.go b/adapters/remote/vmess.go new file mode 100644 index 00000000..fafe2ca6 --- /dev/null +++ b/adapters/remote/vmess.go @@ -0,0 +1,93 @@ +package adapters + +import ( + "fmt" + "net" + "strconv" + "strings" + + "github.com/Dreamacro/clash/common/vmess" + C "github.com/Dreamacro/clash/constant" +) + +// VmessAdapter is a vmess adapter +type VmessAdapter struct { + conn net.Conn +} + +// Close is used to close connection +func (v *VmessAdapter) Close() { + v.conn.Close() +} + +func (v *VmessAdapter) Conn() net.Conn { + return v.conn +} + +type Vmess struct { + name string + server string + client *vmess.Client +} + +func (ss *Vmess) Name() string { + return ss.name +} + +func (ss *Vmess) Type() C.AdapterType { + return C.Vmess +} + +func (ss *Vmess) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) { + c, err := net.Dial("tcp", ss.server) + if err != nil { + return nil, fmt.Errorf("%s connect error", ss.server) + } + tcpKeepAlive(c) + c = ss.client.New(c, parseVmessAddr(addr)) + return &VmessAdapter{conn: c}, err +} + +func NewVmess(name string, server string, uuid string, alterID uint16, security string) (*Vmess, error) { + security = strings.ToLower(security) + client, err := vmess.NewClient(vmess.Config{ + UUID: uuid, + AlterID: alterID, + Security: security, + }) + if err != nil { + return nil, err + } + return &Vmess{ + name: name, + server: server, + client: client, + }, nil +} + +func parseVmessAddr(info *C.Addr) *vmess.DstAddr { + var addrType byte + var addr []byte + switch info.AddrType { + case C.AtypIPv4: + addrType = byte(vmess.AtypIPv4) + addr = make([]byte, net.IPv4len) + copy(addr[:], info.IP.To4()) + case C.AtypIPv6: + addrType = byte(vmess.AtypIPv6) + addr = make([]byte, net.IPv6len) + copy(addr[:], info.IP.To16()) + case C.AtypDomainName: + addrType = byte(vmess.AtypDomainName) + addr = make([]byte, len(info.Host)+1) + addr[0] = byte(len(info.Host)) + copy(addr[1:], []byte(info.Host)) + } + + port, _ := strconv.Atoi(info.Port) + return &vmess.DstAddr{ + AddrType: addrType, + Addr: addr, + Port: uint(port), + } +} diff --git a/common/vmess/aead.go b/common/vmess/aead.go new file mode 100644 index 00000000..342f373e --- /dev/null +++ b/common/vmess/aead.go @@ -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 +} diff --git a/common/vmess/chunk.go b/common/vmess/chunk.go new file mode 100644 index 00000000..b396fdd6 --- /dev/null +++ b/common/vmess/chunk.go @@ -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 +} diff --git a/common/vmess/conn.go b/common/vmess/conn.go new file mode 100644 index 00000000..8a284b28 --- /dev/null +++ b/common/vmess/conn.go @@ -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, + } +} diff --git a/common/vmess/user.go b/common/vmess/user.go new file mode 100644 index 00000000..a1a3f3c2 --- /dev/null +++ b/common/vmess/user.go @@ -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 +} diff --git a/common/vmess/vmess.go b/common/vmess/vmess.go new file mode 100644 index 00000000..464b6c66 --- /dev/null +++ b/common/vmess/vmess.go @@ -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 +} diff --git a/config/config.go b/config/config.go index 88c7d865..e40f8b03 100644 --- a/config/config.go +++ b/config/config.go @@ -236,6 +236,21 @@ func (c *Config) parseProxies(cfg *ini.File) error { addr := fmt.Sprintf("%s:%s", proxy[1], proxy[2]) socks5 := adapters.NewSocks5(key.Name(), addr) proxies[key.Name()] = socks5 + // vmess, server, port, uuid, alterId, security + case "vmess": + if len(proxy) < 6 { + continue + } + addr := fmt.Sprintf("%s:%s", proxy[1], proxy[2]) + alterID, err := strconv.Atoi(proxy[4]) + if err != nil { + return err + } + vmess, err := adapters.NewVmess(key.Name(), addr, proxy[3], uint16(alterID), proxy[5]) + if err != nil { + return err + } + proxies[key.Name()] = vmess } } diff --git a/constant/adapters.go b/constant/adapters.go index a899d54c..a7fd1557 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -12,6 +12,7 @@ const ( Shadowsocks Socks5 URLTest + Vmess ) type ProxyAdapter interface { @@ -47,6 +48,8 @@ func (at AdapterType) String() string { return "Socks5" case URLTest: return "URLTest" + case Vmess: + return "Vmess" default: return "Unknow" }