From 09d49bac95c42f1b30c7791fa79bd0551c8cf9f3 Mon Sep 17 00:00:00 2001 From: Dreamacro <8615343+Dreamacro@users.noreply.github.com> Date: Tue, 24 May 2022 20:28:18 +0800 Subject: [PATCH] Chore: embed shadowsocks2 --- adapter/outbound/shadowsocks.go | 3 +- adapter/outbound/shadowsocksr.go | 7 +- go.mod | 1 - go.sum | 2 - test/go.mod | 1 - test/go.sum | 2 - transport/shadowsocks/README.md | 5 + transport/shadowsocks/core/cipher.go | 164 +++++++++++ transport/shadowsocks/shadowaead/cipher.go | 94 ++++++ transport/shadowsocks/shadowaead/packet.go | 95 +++++++ transport/shadowsocks/shadowaead/stream.go | 285 +++++++++++++++++++ transport/shadowsocks/shadowstream/cipher.go | 116 ++++++++ transport/shadowsocks/shadowstream/packet.go | 79 +++++ transport/shadowsocks/shadowstream/stream.go | 197 +++++++++++++ transport/snell/cipher.go | 3 +- transport/snell/pool.go | 3 +- transport/snell/snell.go | 3 +- transport/ssr/protocol/auth_chain_a.go | 3 +- transport/ssr/protocol/base.go | 3 +- 19 files changed, 1045 insertions(+), 21 deletions(-) create mode 100644 transport/shadowsocks/README.md create mode 100644 transport/shadowsocks/core/cipher.go create mode 100644 transport/shadowsocks/shadowaead/cipher.go create mode 100644 transport/shadowsocks/shadowaead/packet.go create mode 100644 transport/shadowsocks/shadowaead/stream.go create mode 100644 transport/shadowsocks/shadowstream/cipher.go create mode 100644 transport/shadowsocks/shadowstream/packet.go create mode 100644 transport/shadowsocks/shadowstream/stream.go diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index d80c79625..6ed780506 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -10,11 +10,10 @@ import ( "github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/component/dialer" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/shadowsocks/core" obfs "github.com/Dreamacro/clash/transport/simple-obfs" "github.com/Dreamacro/clash/transport/socks5" v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin" - - "github.com/Dreamacro/go-shadowsocks2/core" ) type ShadowSocks struct { diff --git a/adapter/outbound/shadowsocksr.go b/adapter/outbound/shadowsocksr.go index ea1c28384..57ef56046 100644 --- a/adapter/outbound/shadowsocksr.go +++ b/adapter/outbound/shadowsocksr.go @@ -8,12 +8,11 @@ import ( "github.com/Dreamacro/clash/component/dialer" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/shadowsocks/core" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowstream" "github.com/Dreamacro/clash/transport/ssr/obfs" "github.com/Dreamacro/clash/transport/ssr/protocol" - - "github.com/Dreamacro/go-shadowsocks2/core" - "github.com/Dreamacro/go-shadowsocks2/shadowaead" - "github.com/Dreamacro/go-shadowsocks2/shadowstream" ) type ShadowSocksR struct { diff --git a/go.mod b/go.mod index a19ee31c1..6dfc58ac2 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/Dreamacro/clash go 1.18 require ( - github.com/Dreamacro/go-shadowsocks2 v0.1.8 github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/cors v1.2.1 github.com/go-chi/render v1.0.1 diff --git a/go.sum b/go.sum index 8060c2e4a..5ffa36ce6 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/Dreamacro/go-shadowsocks2 v0.1.8 h1:Ixejp5JscEc866gAvm/l6TFd7BOBvDviKgwb1quWw3g= -github.com/Dreamacro/go-shadowsocks2 v0.1.8/go.mod h1:51y4Q6tJoCE7e8TmYXcQRqfoxPfE9Cvn79V6pB6Df7Y= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/test/go.mod b/test/go.mod index ee4074fbd..3af6733c0 100644 --- a/test/go.mod +++ b/test/go.mod @@ -14,7 +14,6 @@ require ( replace github.com/Dreamacro/clash => ../ require ( - github.com/Dreamacro/go-shadowsocks2 v0.1.8 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/distribution v2.8.1+incompatible // indirect diff --git a/test/go.sum b/test/go.sum index fae4729e7..f68bd669b 100644 --- a/test/go.sum +++ b/test/go.sum @@ -1,7 +1,5 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Dreamacro/go-shadowsocks2 v0.1.8 h1:Ixejp5JscEc866gAvm/l6TFd7BOBvDviKgwb1quWw3g= -github.com/Dreamacro/go-shadowsocks2 v0.1.8/go.mod h1:51y4Q6tJoCE7e8TmYXcQRqfoxPfE9Cvn79V6pB6Df7Y= github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= diff --git a/transport/shadowsocks/README.md b/transport/shadowsocks/README.md new file mode 100644 index 000000000..c9fc815c8 --- /dev/null +++ b/transport/shadowsocks/README.md @@ -0,0 +1,5 @@ +## Embedded go-shadowsocks2 + +from https://github.com/Dreamacro/go-shadowsocks2 + +origin https://github.com/riobard/go-shadowsocks2 diff --git a/transport/shadowsocks/core/cipher.go b/transport/shadowsocks/core/cipher.go new file mode 100644 index 000000000..2f5acf631 --- /dev/null +++ b/transport/shadowsocks/core/cipher.go @@ -0,0 +1,164 @@ +package core + +import ( + "crypto/md5" + "errors" + "net" + "sort" + "strings" + + "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowstream" +) + +type Cipher interface { + StreamConnCipher + PacketConnCipher +} + +type StreamConnCipher interface { + StreamConn(net.Conn) net.Conn +} + +type PacketConnCipher interface { + PacketConn(net.PacketConn) net.PacketConn +} + +// ErrCipherNotSupported occurs when a cipher is not supported (likely because of security concerns). +var ErrCipherNotSupported = errors.New("cipher not supported") + +const ( + aeadAes128Gcm = "AEAD_AES_128_GCM" + aeadAes192Gcm = "AEAD_AES_192_GCM" + aeadAes256Gcm = "AEAD_AES_256_GCM" + aeadChacha20Poly1305 = "AEAD_CHACHA20_POLY1305" + aeadXChacha20Poly1305 = "AEAD_XCHACHA20_POLY1305" +) + +// List of AEAD ciphers: key size in bytes and constructor +var aeadList = map[string]struct { + KeySize int + New func([]byte) (shadowaead.Cipher, error) +}{ + aeadAes128Gcm: {16, shadowaead.AESGCM}, + aeadAes192Gcm: {24, shadowaead.AESGCM}, + aeadAes256Gcm: {32, shadowaead.AESGCM}, + aeadChacha20Poly1305: {32, shadowaead.Chacha20Poly1305}, + aeadXChacha20Poly1305: {32, shadowaead.XChacha20Poly1305}, +} + +// List of stream ciphers: key size in bytes and constructor +var streamList = map[string]struct { + KeySize int + New func(key []byte) (shadowstream.Cipher, error) +}{ + "RC4-MD5": {16, shadowstream.RC4MD5}, + "AES-128-CTR": {16, shadowstream.AESCTR}, + "AES-192-CTR": {24, shadowstream.AESCTR}, + "AES-256-CTR": {32, shadowstream.AESCTR}, + "AES-128-CFB": {16, shadowstream.AESCFB}, + "AES-192-CFB": {24, shadowstream.AESCFB}, + "AES-256-CFB": {32, shadowstream.AESCFB}, + "CHACHA20-IETF": {32, shadowstream.Chacha20IETF}, + "XCHACHA20": {32, shadowstream.Xchacha20}, +} + +// ListCipher returns a list of available cipher names sorted alphabetically. +func ListCipher() []string { + var l []string + for k := range aeadList { + l = append(l, k) + } + for k := range streamList { + l = append(l, k) + } + sort.Strings(l) + return l +} + +// PickCipher returns a Cipher of the given name. Derive key from password if given key is empty. +func PickCipher(name string, key []byte, password string) (Cipher, error) { + name = strings.ToUpper(name) + + switch name { + case "DUMMY": + return &dummy{}, nil + case "CHACHA20-IETF-POLY1305": + name = aeadChacha20Poly1305 + case "XCHACHA20-IETF-POLY1305": + name = aeadXChacha20Poly1305 + case "AES-128-GCM": + name = aeadAes128Gcm + case "AES-192-GCM": + name = aeadAes192Gcm + case "AES-256-GCM": + name = aeadAes256Gcm + } + + if choice, ok := aeadList[name]; ok { + if len(key) == 0 { + key = Kdf(password, choice.KeySize) + } + if len(key) != choice.KeySize { + return nil, shadowaead.KeySizeError(choice.KeySize) + } + aead, err := choice.New(key) + return &AeadCipher{Cipher: aead, Key: key}, err + } + + if choice, ok := streamList[name]; ok { + if len(key) == 0 { + key = Kdf(password, choice.KeySize) + } + if len(key) != choice.KeySize { + return nil, shadowstream.KeySizeError(choice.KeySize) + } + ciph, err := choice.New(key) + return &StreamCipher{Cipher: ciph, Key: key}, err + } + + return nil, ErrCipherNotSupported +} + +type AeadCipher struct { + shadowaead.Cipher + + Key []byte +} + +func (aead *AeadCipher) StreamConn(c net.Conn) net.Conn { return shadowaead.NewConn(c, aead) } +func (aead *AeadCipher) PacketConn(c net.PacketConn) net.PacketConn { + return shadowaead.NewPacketConn(c, aead) +} + +type StreamCipher struct { + shadowstream.Cipher + + Key []byte +} + +func (ciph *StreamCipher) StreamConn(c net.Conn) net.Conn { return shadowstream.NewConn(c, ciph) } +func (ciph *StreamCipher) PacketConn(c net.PacketConn) net.PacketConn { + return shadowstream.NewPacketConn(c, ciph) +} + +// dummy cipher does not encrypt + +type dummy struct{} + +func (dummy) StreamConn(c net.Conn) net.Conn { return c } +func (dummy) PacketConn(c net.PacketConn) net.PacketConn { return c } + +// key-derivation function from original Shadowsocks +func Kdf(password string, keyLen int) []byte { + var b, prev []byte + h := md5.New() + for len(b) < keyLen { + h.Write(prev) + h.Write([]byte(password)) + b = h.Sum(b) + prev = b[len(b)-h.Size():] + h.Reset() + } + return b[:keyLen] +} diff --git a/transport/shadowsocks/shadowaead/cipher.go b/transport/shadowsocks/shadowaead/cipher.go new file mode 100644 index 000000000..3cf757497 --- /dev/null +++ b/transport/shadowsocks/shadowaead/cipher.go @@ -0,0 +1,94 @@ +package shadowaead + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha1" + "io" + "strconv" + + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/hkdf" +) + +type Cipher interface { + KeySize() int + SaltSize() int + Encrypter(salt []byte) (cipher.AEAD, error) + Decrypter(salt []byte) (cipher.AEAD, error) +} + +type KeySizeError int + +func (e KeySizeError) Error() string { + return "key size error: need " + strconv.Itoa(int(e)) + " bytes" +} + +func hkdfSHA1(secret, salt, info, outkey []byte) { + r := hkdf.New(sha1.New, secret, salt, info) + if _, err := io.ReadFull(r, outkey); err != nil { + panic(err) // should never happen + } +} + +type metaCipher struct { + psk []byte + makeAEAD func(key []byte) (cipher.AEAD, error) +} + +func (a *metaCipher) KeySize() int { return len(a.psk) } +func (a *metaCipher) SaltSize() int { + if ks := a.KeySize(); ks > 16 { + return ks + } + return 16 +} + +func (a *metaCipher) Encrypter(salt []byte) (cipher.AEAD, error) { + subkey := make([]byte, a.KeySize()) + hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey) + return a.makeAEAD(subkey) +} + +func (a *metaCipher) Decrypter(salt []byte) (cipher.AEAD, error) { + subkey := make([]byte, a.KeySize()) + hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey) + return a.makeAEAD(subkey) +} + +func aesGCM(key []byte) (cipher.AEAD, error) { + blk, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return cipher.NewGCM(blk) +} + +// AESGCM creates a new Cipher with a pre-shared key. len(psk) must be +// one of 16, 24, or 32 to select AES-128/196/256-GCM. +func AESGCM(psk []byte) (Cipher, error) { + switch l := len(psk); l { + case 16, 24, 32: // AES 128/196/256 + default: + return nil, aes.KeySizeError(l) + } + return &metaCipher{psk: psk, makeAEAD: aesGCM}, nil +} + +// Chacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk) +// must be 32. +func Chacha20Poly1305(psk []byte) (Cipher, error) { + if len(psk) != chacha20poly1305.KeySize { + return nil, KeySizeError(chacha20poly1305.KeySize) + } + return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.New}, nil +} + +// XChacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk) +// must be 32. +func XChacha20Poly1305(psk []byte) (Cipher, error) { + if len(psk) != chacha20poly1305.KeySize { + return nil, KeySizeError(chacha20poly1305.KeySize) + } + return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.NewX}, nil +} diff --git a/transport/shadowsocks/shadowaead/packet.go b/transport/shadowsocks/shadowaead/packet.go new file mode 100644 index 000000000..7043ead7c --- /dev/null +++ b/transport/shadowsocks/shadowaead/packet.go @@ -0,0 +1,95 @@ +package shadowaead + +import ( + "crypto/rand" + "errors" + "io" + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +// ErrShortPacket means that the packet is too short for a valid encrypted packet. +var ErrShortPacket = errors.New("short packet") + +var _zerononce [128]byte // read-only. 128 bytes is more than enough. + +// Pack encrypts plaintext using Cipher with a randomly generated salt and +// returns a slice of dst containing the encrypted packet and any error occurred. +// Ensure len(dst) >= ciph.SaltSize() + len(plaintext) + aead.Overhead(). +func Pack(dst, plaintext []byte, ciph Cipher) ([]byte, error) { + saltSize := ciph.SaltSize() + salt := dst[:saltSize] + if _, err := rand.Read(salt); err != nil { + return nil, err + } + aead, err := ciph.Encrypter(salt) + if err != nil { + return nil, err + } + if len(dst) < saltSize+len(plaintext)+aead.Overhead() { + return nil, io.ErrShortBuffer + } + b := aead.Seal(dst[saltSize:saltSize], _zerononce[:aead.NonceSize()], plaintext, nil) + return dst[:saltSize+len(b)], nil +} + +// Unpack decrypts pkt using Cipher and returns a slice of dst containing the decrypted payload and any error occurred. +// Ensure len(dst) >= len(pkt) - aead.SaltSize() - aead.Overhead(). +func Unpack(dst, pkt []byte, ciph Cipher) ([]byte, error) { + saltSize := ciph.SaltSize() + if len(pkt) < saltSize { + return nil, ErrShortPacket + } + salt := pkt[:saltSize] + aead, err := ciph.Decrypter(salt) + if err != nil { + return nil, err + } + if len(pkt) < saltSize+aead.Overhead() { + return nil, ErrShortPacket + } + if saltSize+len(dst)+aead.Overhead() < len(pkt) { + return nil, io.ErrShortBuffer + } + b, err := aead.Open(dst[:0], _zerononce[:aead.NonceSize()], pkt[saltSize:], nil) + return b, err +} + +type PacketConn struct { + net.PacketConn + Cipher +} + +const maxPacketSize = 64 * 1024 + +// NewPacketConn wraps a net.PacketConn with cipher +func NewPacketConn(c net.PacketConn, ciph Cipher) *PacketConn { + return &PacketConn{PacketConn: c, Cipher: ciph} +} + +// WriteTo encrypts b and write to addr using the embedded PacketConn. +func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + buf := pool.Get(maxPacketSize) + defer pool.Put(buf) + buf, err := Pack(buf, b, c) + if err != nil { + return 0, err + } + _, err = c.PacketConn.WriteTo(buf, addr) + return len(b), err +} + +// ReadFrom reads from the embedded PacketConn and decrypts into b. +func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + n, addr, err := c.PacketConn.ReadFrom(b) + if err != nil { + return n, addr, err + } + bb, err := Unpack(b[c.Cipher.SaltSize():], b[:n], c) + if err != nil { + return n, addr, err + } + copy(b, bb) + return len(bb), addr, err +} diff --git a/transport/shadowsocks/shadowaead/stream.go b/transport/shadowsocks/shadowaead/stream.go new file mode 100644 index 000000000..e92bddabb --- /dev/null +++ b/transport/shadowsocks/shadowaead/stream.go @@ -0,0 +1,285 @@ +package shadowaead + +import ( + "crypto/cipher" + "crypto/rand" + "errors" + "io" + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +const ( + // payloadSizeMask is the maximum size of payload in bytes. + payloadSizeMask = 0x3FFF // 16*1024 - 1 + bufSize = 17 * 1024 // >= 2+aead.Overhead()+payloadSizeMask+aead.Overhead() +) + +var ErrZeroChunk = errors.New("zero chunk") + +type Writer struct { + io.Writer + cipher.AEAD + nonce [32]byte // should be sufficient for most nonce sizes +} + +// NewWriter wraps an io.Writer with authenticated encryption. +func NewWriter(w io.Writer, aead cipher.AEAD) *Writer { return &Writer{Writer: w, AEAD: aead} } + +// Write encrypts p and writes to the embedded io.Writer. +func (w *Writer) Write(p []byte) (n int, err error) { + buf := pool.Get(bufSize) + defer pool.Put(buf) + nonce := w.nonce[:w.NonceSize()] + tag := w.Overhead() + off := 2 + tag + + // compatible with snell + if len(p) == 0 { + buf = buf[:off] + buf[0], buf[1] = byte(0), byte(0) + w.Seal(buf[:0], nonce, buf[:2], nil) + increment(nonce) + _, err = w.Writer.Write(buf) + return + } + + for nr := 0; n < len(p) && err == nil; n += nr { + nr = payloadSizeMask + if n+nr > len(p) { + nr = len(p) - n + } + buf = buf[:off+nr+tag] + buf[0], buf[1] = byte(nr>>8), byte(nr) // big-endian payload size + w.Seal(buf[:0], nonce, buf[:2], nil) + increment(nonce) + w.Seal(buf[:off], nonce, p[n:n+nr], nil) + increment(nonce) + _, err = w.Writer.Write(buf) + } + return +} + +// ReadFrom reads from the given io.Reader until EOF or error, encrypts and +// writes to the embedded io.Writer. Returns number of bytes read from r and +// any error encountered. +func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) { + buf := pool.Get(bufSize) + defer pool.Put(buf) + nonce := w.nonce[:w.NonceSize()] + tag := w.Overhead() + off := 2 + tag + for { + nr, er := r.Read(buf[off : off+payloadSizeMask]) + n += int64(nr) + buf[0], buf[1] = byte(nr>>8), byte(nr) + w.Seal(buf[:0], nonce, buf[:2], nil) + increment(nonce) + w.Seal(buf[:off], nonce, buf[off:off+nr], nil) + increment(nonce) + if _, ew := w.Writer.Write(buf[:off+nr+tag]); ew != nil { + err = ew + return + } + if er != nil { + if er != io.EOF { // ignore EOF as per io.ReaderFrom contract + err = er + } + return + } + } +} + +type Reader struct { + io.Reader + cipher.AEAD + nonce [32]byte // should be sufficient for most nonce sizes + buf []byte // to be put back into bufPool + off int // offset to unconsumed part of buf +} + +// NewReader wraps an io.Reader with authenticated decryption. +func NewReader(r io.Reader, aead cipher.AEAD) *Reader { return &Reader{Reader: r, AEAD: aead} } + +// Read and decrypt a record into p. len(p) >= max payload size + AEAD overhead. +func (r *Reader) read(p []byte) (int, error) { + nonce := r.nonce[:r.NonceSize()] + tag := r.Overhead() + + // decrypt payload size + p = p[:2+tag] + if _, err := io.ReadFull(r.Reader, p); err != nil { + return 0, err + } + _, err := r.Open(p[:0], nonce, p, nil) + increment(nonce) + if err != nil { + return 0, err + } + + // decrypt payload + size := (int(p[0])<<8 + int(p[1])) & payloadSizeMask + if size == 0 { + return 0, ErrZeroChunk + } + + p = p[:size+tag] + if _, err := io.ReadFull(r.Reader, p); err != nil { + return 0, err + } + _, err = r.Open(p[:0], nonce, p, nil) + increment(nonce) + if err != nil { + return 0, err + } + return size, nil +} + +// Read reads from the embedded io.Reader, decrypts and writes to p. +func (r *Reader) Read(p []byte) (int, error) { + if r.buf == nil { + if len(p) >= payloadSizeMask+r.Overhead() { + return r.read(p) + } + b := pool.Get(bufSize) + n, err := r.read(b) + if err != nil { + return 0, err + } + r.buf = b[:n] + r.off = 0 + } + + n := copy(p, r.buf[r.off:]) + r.off += n + if r.off == len(r.buf) { + pool.Put(r.buf[:cap(r.buf)]) + r.buf = nil + } + return n, nil +} + +// WriteTo reads from the embedded io.Reader, decrypts and writes to w until +// there's no more data to write or when an error occurs. Return number of +// bytes written to w and any error encountered. +func (r *Reader) WriteTo(w io.Writer) (n int64, err error) { + if r.buf == nil { + r.buf = pool.Get(bufSize) + r.off = len(r.buf) + } + + for { + for r.off < len(r.buf) { + nw, ew := w.Write(r.buf[r.off:]) + r.off += nw + n += int64(nw) + if ew != nil { + if r.off == len(r.buf) { + pool.Put(r.buf[:cap(r.buf)]) + r.buf = nil + } + err = ew + return + } + } + + nr, er := r.read(r.buf) + if er != nil { + if er != io.EOF { + err = er + } + return + } + r.buf = r.buf[:nr] + r.off = 0 + } +} + +// increment little-endian encoded unsigned integer b. Wrap around on overflow. +func increment(b []byte) { + for i := range b { + b[i]++ + if b[i] != 0 { + return + } + } +} + +type Conn struct { + net.Conn + Cipher + r *Reader + w *Writer +} + +// NewConn wraps a stream-oriented net.Conn with cipher. +func NewConn(c net.Conn, ciph Cipher) *Conn { return &Conn{Conn: c, Cipher: ciph} } + +func (c *Conn) initReader() error { + salt := make([]byte, c.SaltSize()) + if _, err := io.ReadFull(c.Conn, salt); err != nil { + return err + } + + aead, err := c.Decrypter(salt) + if err != nil { + return err + } + + c.r = NewReader(c.Conn, aead) + return nil +} + +func (c *Conn) Read(b []byte) (int, error) { + if c.r == nil { + if err := c.initReader(); err != nil { + return 0, err + } + } + return c.r.Read(b) +} + +func (c *Conn) WriteTo(w io.Writer) (int64, error) { + if c.r == nil { + if err := c.initReader(); err != nil { + return 0, err + } + } + return c.r.WriteTo(w) +} + +func (c *Conn) initWriter() error { + salt := make([]byte, c.SaltSize()) + if _, err := rand.Read(salt); err != nil { + return err + } + aead, err := c.Encrypter(salt) + if err != nil { + return err + } + _, err = c.Conn.Write(salt) + if err != nil { + return err + } + c.w = NewWriter(c.Conn, aead) + return nil +} + +func (c *Conn) Write(b []byte) (int, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + return c.w.Write(b) +} + +func (c *Conn) ReadFrom(r io.Reader) (int64, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + return c.w.ReadFrom(r) +} diff --git a/transport/shadowsocks/shadowstream/cipher.go b/transport/shadowsocks/shadowstream/cipher.go new file mode 100644 index 000000000..dd39d03b0 --- /dev/null +++ b/transport/shadowsocks/shadowstream/cipher.go @@ -0,0 +1,116 @@ +package shadowstream + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/rc4" + "strconv" + + "golang.org/x/crypto/chacha20" +) + +// Cipher generates a pair of stream ciphers for encryption and decryption. +type Cipher interface { + IVSize() int + Encrypter(iv []byte) cipher.Stream + Decrypter(iv []byte) cipher.Stream +} + +type KeySizeError int + +func (e KeySizeError) Error() string { + return "key size error: need " + strconv.Itoa(int(e)) + " bytes" +} + +// CTR mode +type ctrStream struct{ cipher.Block } + +func (b *ctrStream) IVSize() int { return b.BlockSize() } +func (b *ctrStream) Decrypter(iv []byte) cipher.Stream { return b.Encrypter(iv) } +func (b *ctrStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCTR(b, iv) } + +func AESCTR(key []byte) (Cipher, error) { + blk, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return &ctrStream{blk}, nil +} + +// CFB mode +type cfbStream struct{ cipher.Block } + +func (b *cfbStream) IVSize() int { return b.BlockSize() } +func (b *cfbStream) Decrypter(iv []byte) cipher.Stream { return cipher.NewCFBDecrypter(b, iv) } +func (b *cfbStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCFBEncrypter(b, iv) } + +func AESCFB(key []byte) (Cipher, error) { + blk, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return &cfbStream{blk}, nil +} + +// IETF-variant of chacha20 +type chacha20ietfkey []byte + +func (k chacha20ietfkey) IVSize() int { return chacha20.NonceSize } +func (k chacha20ietfkey) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) } +func (k chacha20ietfkey) Encrypter(iv []byte) cipher.Stream { + ciph, err := chacha20.NewUnauthenticatedCipher(k, iv) + if err != nil { + panic(err) // should never happen + } + return ciph +} + +func Chacha20IETF(key []byte) (Cipher, error) { + if len(key) != chacha20.KeySize { + return nil, KeySizeError(chacha20.KeySize) + } + return chacha20ietfkey(key), nil +} + +type xchacha20key []byte + +func (k xchacha20key) IVSize() int { return chacha20.NonceSizeX } +func (k xchacha20key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) } +func (k xchacha20key) Encrypter(iv []byte) cipher.Stream { + ciph, err := chacha20.NewUnauthenticatedCipher(k, iv) + if err != nil { + panic(err) // should never happen + } + return ciph +} + +func Xchacha20(key []byte) (Cipher, error) { + if len(key) != chacha20.KeySize { + return nil, KeySizeError(chacha20.KeySize) + } + return xchacha20key(key), nil +} + +type rc4Md5Key []byte + +func (k rc4Md5Key) IVSize() int { + return 16 +} + +func (k rc4Md5Key) Encrypter(iv []byte) cipher.Stream { + h := md5.New() + h.Write([]byte(k)) + h.Write(iv) + rc4key := h.Sum(nil) + c, _ := rc4.NewCipher(rc4key) + return c +} + +func (k rc4Md5Key) Decrypter(iv []byte) cipher.Stream { + return k.Encrypter(iv) +} + +func RC4MD5(key []byte) (Cipher, error) { + return rc4Md5Key(key), nil +} diff --git a/transport/shadowsocks/shadowstream/packet.go b/transport/shadowsocks/shadowstream/packet.go new file mode 100644 index 000000000..0b46dea15 --- /dev/null +++ b/transport/shadowsocks/shadowstream/packet.go @@ -0,0 +1,79 @@ +package shadowstream + +import ( + "crypto/rand" + "errors" + "io" + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +// ErrShortPacket means the packet is too short to be a valid encrypted packet. +var ErrShortPacket = errors.New("short packet") + +// Pack encrypts plaintext using stream cipher s and a random IV. +// Returns a slice of dst containing random IV and ciphertext. +// Ensure len(dst) >= s.IVSize() + len(plaintext). +func Pack(dst, plaintext []byte, s Cipher) ([]byte, error) { + if len(dst) < s.IVSize()+len(plaintext) { + return nil, io.ErrShortBuffer + } + iv := dst[:s.IVSize()] + _, err := rand.Read(iv) + if err != nil { + return nil, err + } + s.Encrypter(iv).XORKeyStream(dst[len(iv):], plaintext) + return dst[:len(iv)+len(plaintext)], nil +} + +// Unpack decrypts pkt using stream cipher s. +// Returns a slice of dst containing decrypted plaintext. +func Unpack(dst, pkt []byte, s Cipher) ([]byte, error) { + if len(pkt) < s.IVSize() { + return nil, ErrShortPacket + } + if len(dst) < len(pkt)-s.IVSize() { + return nil, io.ErrShortBuffer + } + iv := pkt[:s.IVSize()] + s.Decrypter(iv).XORKeyStream(dst, pkt[len(iv):]) + return dst[:len(pkt)-len(iv)], nil +} + +type PacketConn struct { + net.PacketConn + Cipher +} + +// NewPacketConn wraps a net.PacketConn with stream cipher encryption/decryption. +func NewPacketConn(c net.PacketConn, ciph Cipher) *PacketConn { + return &PacketConn{PacketConn: c, Cipher: ciph} +} + +const maxPacketSize = 64 * 1024 + +func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + buf := pool.Get(maxPacketSize) + defer pool.Put(buf) + buf, err := Pack(buf, b, c.Cipher) + if err != nil { + return 0, err + } + _, err = c.PacketConn.WriteTo(buf, addr) + return len(b), err +} + +func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + n, addr, err := c.PacketConn.ReadFrom(b) + if err != nil { + return n, addr, err + } + bb, err := Unpack(b[c.IVSize():], b[:n], c.Cipher) + if err != nil { + return n, addr, err + } + copy(b, bb) + return len(bb), addr, err +} diff --git a/transport/shadowsocks/shadowstream/stream.go b/transport/shadowsocks/shadowstream/stream.go new file mode 100644 index 000000000..6c4b0f6da --- /dev/null +++ b/transport/shadowsocks/shadowstream/stream.go @@ -0,0 +1,197 @@ +package shadowstream + +import ( + "crypto/cipher" + "crypto/rand" + "io" + "net" +) + +const bufSize = 2048 + +type Writer struct { + io.Writer + cipher.Stream + buf [bufSize]byte +} + +// NewWriter wraps an io.Writer with stream cipher encryption. +func NewWriter(w io.Writer, s cipher.Stream) *Writer { return &Writer{Writer: w, Stream: s} } + +func (w *Writer) Write(p []byte) (n int, err error) { + buf := w.buf[:] + for nw := 0; n < len(p) && err == nil; n += nw { + end := n + len(buf) + if end > len(p) { + end = len(p) + } + w.XORKeyStream(buf, p[n:end]) + nw, err = w.Writer.Write(buf[:end-n]) + } + return +} + +func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) { + buf := w.buf[:] + for { + nr, er := r.Read(buf) + n += int64(nr) + b := buf[:nr] + w.XORKeyStream(b, b) + if _, err = w.Writer.Write(b); err != nil { + return + } + if er != nil { + if er != io.EOF { // ignore EOF as per io.ReaderFrom contract + err = er + } + return + } + } +} + +type Reader struct { + io.Reader + cipher.Stream + buf [bufSize]byte +} + +// NewReader wraps an io.Reader with stream cipher decryption. +func NewReader(r io.Reader, s cipher.Stream) *Reader { return &Reader{Reader: r, Stream: s} } + +func (r *Reader) Read(p []byte) (n int, err error) { + n, err = r.Reader.Read(p) + if err != nil { + return 0, err + } + r.XORKeyStream(p, p[:n]) + return +} + +func (r *Reader) WriteTo(w io.Writer) (n int64, err error) { + buf := r.buf[:] + for { + nr, er := r.Reader.Read(buf) + if nr > 0 { + r.XORKeyStream(buf, buf[:nr]) + nw, ew := w.Write(buf[:nr]) + n += int64(nw) + if ew != nil { + err = ew + return + } + } + if er != nil { + if er != io.EOF { // ignore EOF as per io.Copy contract (using src.WriteTo shortcut) + err = er + } + return + } + } +} + +// A Conn represents a Shadowsocks connection. It implements the net.Conn interface. +type Conn struct { + net.Conn + Cipher + r *Reader + w *Writer + readIV []byte + writeIV []byte +} + +// NewConn wraps a stream-oriented net.Conn with stream cipher encryption/decryption. +func NewConn(c net.Conn, ciph Cipher) *Conn { return &Conn{Conn: c, Cipher: ciph} } + +func (c *Conn) initReader() error { + if c.r == nil { + iv, err := c.ObtainReadIV() + if err != nil { + return err + } + c.r = NewReader(c.Conn, c.Decrypter(iv)) + } + return nil +} + +func (c *Conn) Read(b []byte) (int, error) { + if c.r == nil { + if err := c.initReader(); err != nil { + return 0, err + } + } + return c.r.Read(b) +} + +func (c *Conn) WriteTo(w io.Writer) (int64, error) { + if c.r == nil { + if err := c.initReader(); err != nil { + return 0, err + } + } + return c.r.WriteTo(w) +} + +func (c *Conn) initWriter() error { + if c.w == nil { + iv, err := c.ObtainWriteIV() + if err != nil { + return err + } + if _, err := c.Conn.Write(iv); err != nil { + return err + } + c.w = NewWriter(c.Conn, c.Encrypter(iv)) + } + return nil +} + +func (c *Conn) Write(b []byte) (int, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + return c.w.Write(b) +} + +func (c *Conn) ReadFrom(r io.Reader) (int64, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + return c.w.ReadFrom(r) +} + +func (c *Conn) ObtainWriteIV() ([]byte, error) { + if len(c.writeIV) == c.IVSize() { + return c.writeIV, nil + } + + iv := make([]byte, c.IVSize()) + + if _, err := rand.Read(iv); err != nil { + return nil, err + } + + c.writeIV = iv + + return iv, nil +} + +func (c *Conn) ObtainReadIV() ([]byte, error) { + if len(c.readIV) == c.IVSize() { + return c.readIV, nil + } + + iv := make([]byte, c.IVSize()) + + if _, err := io.ReadFull(c.Conn, iv); err != nil { + return nil, err + } + + c.readIV = iv + + return iv, nil +} diff --git a/transport/snell/cipher.go b/transport/snell/cipher.go index 0f31aea53..24999e283 100644 --- a/transport/snell/cipher.go +++ b/transport/snell/cipher.go @@ -4,7 +4,8 @@ import ( "crypto/aes" "crypto/cipher" - "github.com/Dreamacro/go-shadowsocks2/shadowaead" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" + "golang.org/x/crypto/argon2" "golang.org/x/crypto/chacha20poly1305" ) diff --git a/transport/snell/pool.go b/transport/snell/pool.go index 62d21b4e8..54b923449 100644 --- a/transport/snell/pool.go +++ b/transport/snell/pool.go @@ -6,8 +6,7 @@ import ( "time" "github.com/Dreamacro/clash/component/pool" - - "github.com/Dreamacro/go-shadowsocks2/shadowaead" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" ) type Pool struct { diff --git a/transport/snell/snell.go b/transport/snell/snell.go index 4cd5fba8d..c6b7a5690 100644 --- a/transport/snell/snell.go +++ b/transport/snell/snell.go @@ -9,9 +9,8 @@ import ( "sync" "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" "github.com/Dreamacro/clash/transport/socks5" - - "github.com/Dreamacro/go-shadowsocks2/shadowaead" ) const ( diff --git a/transport/ssr/protocol/auth_chain_a.go b/transport/ssr/protocol/auth_chain_a.go index 906f8deb3..6b12ab9bc 100644 --- a/transport/ssr/protocol/auth_chain_a.go +++ b/transport/ssr/protocol/auth_chain_a.go @@ -13,9 +13,8 @@ import ( "github.com/Dreamacro/clash/common/pool" "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/transport/shadowsocks/core" "github.com/Dreamacro/clash/transport/ssr/tools" - - "github.com/Dreamacro/go-shadowsocks2/core" ) func init() { diff --git a/transport/ssr/protocol/base.go b/transport/ssr/protocol/base.go index 1ed348d00..4bf799b35 100644 --- a/transport/ssr/protocol/base.go +++ b/transport/ssr/protocol/base.go @@ -12,8 +12,7 @@ import ( "github.com/Dreamacro/clash/common/pool" "github.com/Dreamacro/clash/log" - - "github.com/Dreamacro/go-shadowsocks2/core" + "github.com/Dreamacro/clash/transport/shadowsocks/core" ) type Base struct {