Chore: split component to transport
This commit is contained in:
124
transport/vmess/aead.go
Normal file
124
transport/vmess/aead.go
Normal file
@ -0,0 +1,124 @@
|
||||
package vmess
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/Dreamacro/clash/common/pool"
|
||||
)
|
||||
|
||||
type aeadWriter struct {
|
||||
io.Writer
|
||||
cipher.AEAD
|
||||
nonce [32]byte
|
||||
count uint16
|
||||
iv []byte
|
||||
|
||||
writeLock sync.Mutex
|
||||
}
|
||||
|
||||
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) {
|
||||
w.writeLock.Lock()
|
||||
buf := pool.Get(pool.RelayBufferSize)
|
||||
defer func() {
|
||||
w.writeLock.Unlock()
|
||||
pool.Put(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) {
|
||||
pool.Put(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 := pool.Get(size)
|
||||
_, err = io.ReadFull(r.Reader, buf[:size])
|
||||
if err != nil {
|
||||
pool.Put(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 {
|
||||
pool.Put(buf)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
r.offset = n
|
||||
r.buf = buf[:realLen]
|
||||
return n, nil
|
||||
}
|
102
transport/vmess/chunk.go
Normal file
102
transport/vmess/chunk.go
Normal file
@ -0,0 +1,102 @@
|
||||
package vmess
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/Dreamacro/clash/common/pool"
|
||||
)
|
||||
|
||||
const (
|
||||
lenSize = 2
|
||||
chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024
|
||||
maxSize = 17 * 1024 // 2 + chunkSize + aead.Overhead()
|
||||
)
|
||||
|
||||
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) {
|
||||
pool.Put(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 := pool.Get(size)
|
||||
_, err = io.ReadFull(cr.Reader, buf)
|
||||
if err != nil {
|
||||
pool.Put(buf)
|
||||
return 0, err
|
||||
}
|
||||
n := copy(b, buf)
|
||||
cr.offset = n
|
||||
cr.buf = buf
|
||||
return n, nil
|
||||
}
|
||||
|
||||
type chunkWriter struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (cw *chunkWriter) Write(b []byte) (n int, err error) {
|
||||
buf := pool.Get(pool.RelayBufferSize)
|
||||
defer pool.Put(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
|
||||
}
|
275
transport/vmess/conn.go
Normal file
275
transport/vmess/conn.go
Normal file
@ -0,0 +1,275 @@
|
||||
package vmess
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"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
|
||||
isAead bool
|
||||
|
||||
received bool
|
||||
}
|
||||
|
||||
func (vc *Conn) Write(b []byte) (int, error) {
|
||||
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 := time.Now()
|
||||
|
||||
if !vc.isAead {
|
||||
h := hmac.New(md5.New, vc.id.UUID.Bytes())
|
||||
binary.Write(h, binary.BigEndian, uint64(timestamp.Unix()))
|
||||
if _, err := vc.Conn.Write(h.Sum(nil)); 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)
|
||||
if vc.dst.UDP {
|
||||
buf.WriteByte(CommandUDP)
|
||||
} else {
|
||||
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))
|
||||
|
||||
if !vc.isAead {
|
||||
block, err := aes.NewCipher(vc.id.CmdKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stream := cipher.NewCFBEncrypter(block, hashTimestamp(timestamp))
|
||||
stream.XORKeyStream(buf.Bytes(), buf.Bytes())
|
||||
_, err = vc.Conn.Write(buf.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
var fixedLengthCmdKey [16]byte
|
||||
copy(fixedLengthCmdKey[:], vc.id.CmdKey)
|
||||
vmessout := sealVMessAEADHeader(fixedLengthCmdKey, buf.Bytes(), timestamp)
|
||||
_, err := vc.Conn.Write(vmessout)
|
||||
return err
|
||||
}
|
||||
|
||||
func (vc *Conn) recvResponse() error {
|
||||
var buf []byte
|
||||
if !vc.isAead {
|
||||
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)
|
||||
} else {
|
||||
aeadResponseHeaderLengthEncryptionKey := kdf(vc.respBodyKey[:], kdfSaltConstAEADRespHeaderLenKey)[:16]
|
||||
aeadResponseHeaderLengthEncryptionIV := kdf(vc.respBodyIV[:], kdfSaltConstAEADRespHeaderLenIV)[:12]
|
||||
|
||||
aeadResponseHeaderLengthEncryptionKeyAESBlock, _ := aes.NewCipher(aeadResponseHeaderLengthEncryptionKey)
|
||||
aeadResponseHeaderLengthEncryptionAEAD, _ := cipher.NewGCM(aeadResponseHeaderLengthEncryptionKeyAESBlock)
|
||||
|
||||
aeadEncryptedResponseHeaderLength := make([]byte, 18)
|
||||
if _, err := io.ReadFull(vc.Conn, aeadEncryptedResponseHeaderLength); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decryptedResponseHeaderLengthBinaryBuffer, err := aeadResponseHeaderLengthEncryptionAEAD.Open(nil, aeadResponseHeaderLengthEncryptionIV, aeadEncryptedResponseHeaderLength[:], nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decryptedResponseHeaderLength := binary.BigEndian.Uint16(decryptedResponseHeaderLengthBinaryBuffer)
|
||||
aeadResponseHeaderPayloadEncryptionKey := kdf(vc.respBodyKey[:], kdfSaltConstAEADRespHeaderPayloadKey)[:16]
|
||||
aeadResponseHeaderPayloadEncryptionIV := kdf(vc.respBodyIV[:], kdfSaltConstAEADRespHeaderPayloadIV)[:12]
|
||||
aeadResponseHeaderPayloadEncryptionKeyAESBlock, _ := aes.NewCipher(aeadResponseHeaderPayloadEncryptionKey)
|
||||
aeadResponseHeaderPayloadEncryptionAEAD, _ := cipher.NewGCM(aeadResponseHeaderPayloadEncryptionKeyAESBlock)
|
||||
|
||||
encryptedResponseHeaderBuffer := make([]byte, decryptedResponseHeaderLength+16)
|
||||
if _, err := io.ReadFull(vc.Conn, encryptedResponseHeaderBuffer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf, err = aeadResponseHeaderPayloadEncryptionAEAD.Open(nil, aeadResponseHeaderPayloadEncryptionIV, encryptedResponseHeaderBuffer, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(buf) < 4 {
|
||||
return errors.New("unexpected buffer length")
|
||||
}
|
||||
}
|
||||
|
||||
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.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, isAead bool) (*Conn, error) {
|
||||
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]
|
||||
|
||||
var (
|
||||
respBodyKey []byte
|
||||
respBodyIV []byte
|
||||
)
|
||||
|
||||
if isAead {
|
||||
bodyKey := sha256.Sum256(reqBodyKey)
|
||||
bodyIV := sha256.Sum256(reqBodyIV)
|
||||
respBodyKey = bodyKey[:16]
|
||||
respBodyIV = bodyIV[:16]
|
||||
} else {
|
||||
bodyKey := md5.Sum(reqBodyKey)
|
||||
bodyIV := md5.Sum(reqBodyIV)
|
||||
respBodyKey = bodyKey[:]
|
||||
respBodyIV = bodyIV[:]
|
||||
}
|
||||
|
||||
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[:])
|
||||
}
|
||||
|
||||
c := &Conn{
|
||||
Conn: conn,
|
||||
id: id,
|
||||
dst: dst,
|
||||
reqBodyIV: reqBodyIV,
|
||||
reqBodyKey: reqBodyKey,
|
||||
respV: respV,
|
||||
respBodyIV: respBodyIV[:],
|
||||
respBodyKey: respBodyKey[:],
|
||||
reader: reader,
|
||||
writer: writer,
|
||||
security: security,
|
||||
isAead: isAead,
|
||||
}
|
||||
if err := c.sendRequest(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
109
transport/vmess/h2.go
Normal file
109
transport/vmess/h2.go
Normal file
@ -0,0 +1,109 @@
|
||||
package vmess
|
||||
|
||||
import (
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type h2Conn struct {
|
||||
net.Conn
|
||||
*http2.ClientConn
|
||||
pwriter *io.PipeWriter
|
||||
res *http.Response
|
||||
cfg *H2Config
|
||||
}
|
||||
|
||||
type H2Config struct {
|
||||
Hosts []string
|
||||
Path string
|
||||
}
|
||||
|
||||
func (hc *h2Conn) establishConn() error {
|
||||
preader, pwriter := io.Pipe()
|
||||
|
||||
host := hc.cfg.Hosts[rand.Intn(len(hc.cfg.Hosts))]
|
||||
path := hc.cfg.Path
|
||||
// TODO: connect use VMess Host instead of H2 Host
|
||||
req := http.Request{
|
||||
Method: "PUT",
|
||||
Host: host,
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: host,
|
||||
Path: path,
|
||||
},
|
||||
Proto: "HTTP/2",
|
||||
ProtoMajor: 2,
|
||||
ProtoMinor: 0,
|
||||
Body: preader,
|
||||
Header: map[string][]string{
|
||||
"Accept-Encoding": {"identity"},
|
||||
},
|
||||
}
|
||||
|
||||
// it will be close at : `func (hc *h2Conn) Close() error`
|
||||
res, err := hc.ClientConn.RoundTrip(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hc.pwriter = pwriter
|
||||
hc.res = res
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read implements net.Conn.Read()
|
||||
func (hc *h2Conn) Read(b []byte) (int, error) {
|
||||
if hc.res != nil && !hc.res.Close {
|
||||
n, err := hc.res.Body.Read(b)
|
||||
return n, err
|
||||
}
|
||||
|
||||
if err := hc.establishConn(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return hc.res.Body.Read(b)
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (hc *h2Conn) Write(b []byte) (int, error) {
|
||||
if hc.pwriter != nil {
|
||||
return hc.pwriter.Write(b)
|
||||
}
|
||||
|
||||
if err := hc.establishConn(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return hc.pwriter.Write(b)
|
||||
}
|
||||
|
||||
func (hc *h2Conn) Close() error {
|
||||
if err := hc.pwriter.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := hc.ClientConn.Shutdown(hc.res.Request.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
return hc.Conn.Close()
|
||||
}
|
||||
|
||||
func StreamH2Conn(conn net.Conn, cfg *H2Config) (net.Conn, error) {
|
||||
transport := &http2.Transport{}
|
||||
|
||||
cconn, err := transport.NewClientConn(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &h2Conn{
|
||||
Conn: conn,
|
||||
ClientConn: cconn,
|
||||
cfg: cfg,
|
||||
}, nil
|
||||
}
|
103
transport/vmess/header.go
Normal file
103
transport/vmess/header.go
Normal file
@ -0,0 +1,103 @@
|
||||
package vmess
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"hash"
|
||||
"hash/crc32"
|
||||
"time"
|
||||
)
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
func sealVMessAEADHeader(key [16]byte, data []byte, t time.Time) []byte {
|
||||
generatedAuthID := createAuthID(key[:], t.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()
|
||||
}
|
77
transport/vmess/http.go
Normal file
77
transport/vmess/http.go
Normal file
@ -0,0 +1,77 @@
|
||||
package vmess
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
)
|
||||
|
||||
type httpConn struct {
|
||||
net.Conn
|
||||
cfg *HTTPConfig
|
||||
reader *bufio.Reader
|
||||
whandshake bool
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
Method string
|
||||
Host string
|
||||
Path []string
|
||||
Headers map[string][]string
|
||||
}
|
||||
|
||||
// Read implements net.Conn.Read()
|
||||
func (hc *httpConn) Read(b []byte) (int, error) {
|
||||
if hc.reader != nil {
|
||||
n, err := hc.reader.Read(b)
|
||||
return n, err
|
||||
}
|
||||
|
||||
reader := textproto.NewConn(hc.Conn)
|
||||
// First line: GET /index.html HTTP/1.0
|
||||
if _, err := reader.ReadLine(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if _, err := reader.ReadMIMEHeader(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
hc.reader = reader.R
|
||||
return reader.R.Read(b)
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (hc *httpConn) Write(b []byte) (int, error) {
|
||||
if hc.whandshake {
|
||||
return hc.Conn.Write(b)
|
||||
}
|
||||
|
||||
path := hc.cfg.Path[rand.Intn(len(hc.cfg.Path))]
|
||||
u := fmt.Sprintf("http://%s%s", hc.cfg.Host, path)
|
||||
req, _ := http.NewRequest("GET", u, bytes.NewBuffer(b))
|
||||
for key, list := range hc.cfg.Headers {
|
||||
req.Header.Set(key, list[rand.Intn(len(list))])
|
||||
}
|
||||
req.ContentLength = int64(len(b))
|
||||
if err := req.Write(hc.Conn); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
hc.whandshake = true
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (hc *httpConn) Close() error {
|
||||
return hc.Conn.Close()
|
||||
}
|
||||
|
||||
func StreamHTTPConn(conn net.Conn, cfg *HTTPConfig) net.Conn {
|
||||
return &httpConn{
|
||||
Conn: conn,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
26
transport/vmess/tls.go
Normal file
26
transport/vmess/tls.go
Normal file
@ -0,0 +1,26 @@
|
||||
package vmess
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
)
|
||||
|
||||
type TLSConfig struct {
|
||||
Host string
|
||||
SkipCertVerify bool
|
||||
SessionCache tls.ClientSessionCache
|
||||
NextProtos []string
|
||||
}
|
||||
|
||||
func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) {
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: cfg.Host,
|
||||
InsecureSkipVerify: cfg.SkipCertVerify,
|
||||
ClientSessionCache: cfg.SessionCache,
|
||||
NextProtos: cfg.NextProtos,
|
||||
}
|
||||
|
||||
tlsConn := tls.Client(conn, tlsConfig)
|
||||
err := tlsConn.Handshake()
|
||||
return tlsConn, err
|
||||
}
|
55
transport/vmess/user.go
Normal file
55
transport/vmess/user.go
Normal file
@ -0,0 +1,55 @@
|
||||
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
|
||||
}
|
||||
alterIDs = append(alterIDs, primary)
|
||||
return alterIDs
|
||||
}
|
113
transport/vmess/vmess.go
Normal file
113
transport/vmess/vmess.go
Normal file
@ -0,0 +1,113 @@
|
||||
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 {
|
||||
UDP bool
|
||||
AddrType byte
|
||||
Addr []byte
|
||||
Port uint
|
||||
}
|
||||
|
||||
// Client is vmess connection generator
|
||||
type Client struct {
|
||||
user []*ID
|
||||
uuid *uuid.UUID
|
||||
security Security
|
||||
isAead bool
|
||||
}
|
||||
|
||||
// Config of vmess
|
||||
type Config struct {
|
||||
UUID string
|
||||
AlterID uint16
|
||||
Security string
|
||||
Port string
|
||||
HostName string
|
||||
IsAead bool
|
||||
}
|
||||
|
||||
// StreamConn return a Conn with net.Conn and DstAddr
|
||||
func (c *Client) StreamConn(conn net.Conn, dst *DstAddr) (net.Conn, error) {
|
||||
r := rand.Intn(len(c.user))
|
||||
return newConn(conn, c.user[r], dst, c.security, c.isAead)
|
||||
}
|
||||
|
||||
// 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,
|
||||
isAead: config.IsAead,
|
||||
}, nil
|
||||
}
|
169
transport/vmess/websocket.go
Normal file
169
transport/vmess/websocket.go
Normal file
@ -0,0 +1,169 @@
|
||||
package vmess
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type websocketConn struct {
|
||||
conn *websocket.Conn
|
||||
reader io.Reader
|
||||
remoteAddr net.Addr
|
||||
|
||||
// https://godoc.org/github.com/gorilla/websocket#hdr-Concurrency
|
||||
rMux sync.Mutex
|
||||
wMux sync.Mutex
|
||||
}
|
||||
|
||||
type WebsocketConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
Path string
|
||||
Headers http.Header
|
||||
TLS bool
|
||||
SkipCertVerify bool
|
||||
ServerName string
|
||||
SessionCache tls.ClientSessionCache
|
||||
}
|
||||
|
||||
// Read implements net.Conn.Read()
|
||||
func (wsc *websocketConn) Read(b []byte) (int, error) {
|
||||
wsc.rMux.Lock()
|
||||
defer wsc.rMux.Unlock()
|
||||
for {
|
||||
reader, err := wsc.getReader()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
nBytes, err := reader.Read(b)
|
||||
if err == io.EOF {
|
||||
wsc.reader = nil
|
||||
continue
|
||||
}
|
||||
return nBytes, err
|
||||
}
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (wsc *websocketConn) Write(b []byte) (int, error) {
|
||||
wsc.wMux.Lock()
|
||||
defer wsc.wMux.Unlock()
|
||||
if err := wsc.conn.WriteMessage(websocket.BinaryMessage, b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (wsc *websocketConn) Close() error {
|
||||
var errors []string
|
||||
if err := wsc.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second*5)); err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
if err := wsc.conn.Close(); err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("failed to close connection: %s", strings.Join(errors, ","))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsc *websocketConn) getReader() (io.Reader, error) {
|
||||
if wsc.reader != nil {
|
||||
return wsc.reader, nil
|
||||
}
|
||||
|
||||
_, reader, err := wsc.conn.NextReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wsc.reader = reader
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func (wsc *websocketConn) LocalAddr() net.Addr {
|
||||
return wsc.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (wsc *websocketConn) RemoteAddr() net.Addr {
|
||||
return wsc.remoteAddr
|
||||
}
|
||||
|
||||
func (wsc *websocketConn) SetDeadline(t time.Time) error {
|
||||
if err := wsc.SetReadDeadline(t); err != nil {
|
||||
return err
|
||||
}
|
||||
return wsc.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (wsc *websocketConn) SetReadDeadline(t time.Time) error {
|
||||
return wsc.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (wsc *websocketConn) SetWriteDeadline(t time.Time) error {
|
||||
return wsc.conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func StreamWebsocketConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) {
|
||||
dialer := &websocket.Dialer{
|
||||
NetDial: func(network, addr string) (net.Conn, error) {
|
||||
return conn, nil
|
||||
},
|
||||
ReadBufferSize: 4 * 1024,
|
||||
WriteBufferSize: 4 * 1024,
|
||||
HandshakeTimeout: time.Second * 8,
|
||||
}
|
||||
|
||||
scheme := "ws"
|
||||
if c.TLS {
|
||||
scheme = "wss"
|
||||
dialer.TLSClientConfig = &tls.Config{
|
||||
ServerName: c.Host,
|
||||
InsecureSkipVerify: c.SkipCertVerify,
|
||||
ClientSessionCache: c.SessionCache,
|
||||
}
|
||||
|
||||
if c.ServerName != "" {
|
||||
dialer.TLSClientConfig.ServerName = c.ServerName
|
||||
} else if host := c.Headers.Get("Host"); host != "" {
|
||||
dialer.TLSClientConfig.ServerName = host
|
||||
}
|
||||
}
|
||||
|
||||
uri := url.URL{
|
||||
Scheme: scheme,
|
||||
Host: net.JoinHostPort(c.Host, c.Port),
|
||||
Path: c.Path,
|
||||
}
|
||||
|
||||
headers := http.Header{}
|
||||
if c.Headers != nil {
|
||||
for k := range c.Headers {
|
||||
headers.Add(k, c.Headers.Get(k))
|
||||
}
|
||||
}
|
||||
|
||||
wsConn, resp, err := dialer.Dial(uri.String(), headers)
|
||||
if err != nil {
|
||||
reason := err.Error()
|
||||
if resp != nil {
|
||||
reason = resp.Status
|
||||
}
|
||||
return nil, fmt.Errorf("dial %s error: %s", uri.Host, reason)
|
||||
}
|
||||
|
||||
return &websocketConn{
|
||||
conn: wsConn,
|
||||
remoteAddr: conn.RemoteAddr(),
|
||||
}, nil
|
||||
}
|
Reference in New Issue
Block a user