diff --git a/README.md b/README.md index 28f2c1c..bb60456 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ glider -h click to see details ```bash -glider 0.12.1 usage: +glider 0.12.2 usage: -checkdisabledonly check disabled fowarders only -checkinterval int @@ -159,7 +159,7 @@ Available methods for ss: AES-128-CFB AES-128-CTR AES-192-CFB AES-192-CTR AES-256-CFB AES-256-CTR CHACHA20-IETF XCHACHA20 CHACHA20 RC4-MD5 Alias: chacha20-ietf-poly1305 = AEAD_CHACHA20_POLY1305, xchacha20-ietf-poly1305 = AEAD_XCHACHA20_POLY1305 - Plain: DUMMY + Plain: NONE SSR scheme: ssr://method:pass@host:port?protocol=xxx&protocol_param=yyy&obfs=zzz&obfs_param=xyz diff --git a/config.go b/config.go index d27259f..07bd968 100644 --- a/config.go +++ b/config.go @@ -150,7 +150,7 @@ func usage() { fmt.Fprintf(w, " AES-128-CFB AES-128-CTR AES-192-CFB AES-192-CTR AES-256-CFB AES-256-CTR CHACHA20-IETF XCHACHA20 CHACHA20 RC4-MD5\n") fmt.Fprintf(w, " Alias:\n") fmt.Fprintf(w, " chacha20-ietf-poly1305 = AEAD_CHACHA20_POLY1305, xchacha20-ietf-poly1305 = AEAD_XCHACHA20_POLY1305\n") - fmt.Fprintf(w, " Plain: DUMMY\n") + fmt.Fprintf(w, " Plain: NONE\n") fmt.Fprintf(w, "\n") fmt.Fprintf(w, "SSR scheme:\n") diff --git a/go.mod b/go.mod index 79f85e0..82cef3f 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,16 @@ module github.com/nadoo/glider go 1.15 require ( + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da github.com/insomniacslk/dhcp v0.0.0-20200922210017-67c425063dca github.com/mzz2017/shadowsocksR v1.0.0 github.com/nadoo/conflag v0.2.3 - github.com/nadoo/go-shadowsocks2 v0.1.2 github.com/nadoo/ipset v0.3.0 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/xtaci/kcp-go/v5 v5.6.1 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 - golang.org/x/net v0.0.0-20201024042810-be3efd7ff127 // indirect - golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd // indirect + golang.org/x/net v0.0.0-20201026091529-146b70c837a4 // indirect + golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5 // indirect golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6 // indirect gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect ) diff --git a/go.sum b/go.sum index 92eed3a..4f19794 100644 --- a/go.sum +++ b/go.sum @@ -141,8 +141,8 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgN golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201024042810-be3efd7ff127 h1:pZPp9+iYUqwYKLjht0SDBbRCRK/9gAXDy7pz5fRDpjo= -golang.org/x/net v0.0.0-20201024042810-be3efd7ff127/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201026091529-146b70c837a4 h1:awiuzyrRjJDb+OXi9ceHO3SDxVoN3JER57mhtqkdQBs= +golang.org/x/net v0.0.0-20201026091529-146b70c837a4/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -168,8 +168,8 @@ golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd h1:WgqgiQvkiZWz7XLhphjt2GI2GcGCTIZs9jqXMWmH+oc= -golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5 h1:iCaAy5bMeEvwANu3YnJfWwI0kWAGkEa2RXPdweI/ysk= +golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go index 9456958..1a1e1cc 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ import ( ) var ( - version = "0.12.1" + version = "0.12.2" config = parseConfig() ) diff --git a/proxy/conn.go b/proxy/conn.go index 98f9c07..f3a51ea 100644 --- a/proxy/conn.go +++ b/proxy/conn.go @@ -36,11 +36,13 @@ func NewConn(c net.Conn) *Conn { } // Reader returns the internal bufio.Reader. -func (c *Conn) Reader() *bufio.Reader { return c.r } +func (c *Conn) Reader() *bufio.Reader { return c.r } +func (c *Conn) Read(p []byte) (int, error) { return c.r.Read(p) } // Peek returns the next n bytes without advancing the reader. -func (c *Conn) Peek(n int) ([]byte, error) { return c.r.Peek(n) } -func (c *Conn) Read(p []byte) (int, error) { return c.r.Read(p) } +func (c *Conn) Peek(n int) ([]byte, error) { return c.r.Peek(n) } + +// WriteTo implements io.WriterTo. func (c *Conn) WriteTo(w io.Writer) (n int64, err error) { return c.r.WriteTo(w) } // Relay relays between left and right. @@ -71,6 +73,30 @@ func Relay(left, right net.Conn) error { return nil } +// Copy copies from src to dst. +func Copy(dst io.Writer, src io.Reader) (written int64, err error) { + dst = underlyingWriter(dst) + switch runtime.GOOS { + case "linux", "windows", "dragonfly", "freebsd", "solaris": + if _, ok := dst.(*net.TCPConn); ok && worthTry(src) { + if wt, ok := src.(io.WriterTo); ok { + return wt.WriteTo(dst) + } + if rt, ok := dst.(io.ReaderFrom); ok { + return rt.ReadFrom(src) + } + } + } + return CopyBuffer(dst, src) +} + +func underlyingWriter(c io.Writer) io.Writer { + if wrap, ok := c.(*Conn); ok { + return wrap.Conn + } + return c +} + func worthTry(src io.Reader) bool { switch v := src.(type) { case *net.TCPConn, *net.UnixConn: @@ -90,30 +116,6 @@ func worthTry(src io.Reader) bool { } } -func underlyingWriter(c io.Writer) io.Writer { - if wrap, ok := c.(*Conn); ok { - return wrap.Conn - } - return c -} - -// Copy copies from src to dst. -func Copy(dst io.Writer, src io.Reader) (written int64, err error) { - dst = underlyingWriter(dst) - switch runtime.GOOS { - case "linux", "windows", "dragonfly", "freebsd", "solaris": - if _, ok := dst.(*net.TCPConn); ok && worthTry(src) { - if wt, ok := src.(io.WriterTo); ok { - return wt.WriteTo(dst) - } - if rt, ok := dst.(io.ReaderFrom); ok { - return rt.ReadFrom(src) - } - } - } - return CopyBuffer(dst, src) -} - // CopyN copies n bytes (or until an error) from src to dst. func CopyN(dst io.Writer, src io.Reader, n int64) (written int64, err error) { written, err = Copy(dst, io.LimitReader(src, n)) diff --git a/proxy/ss/client.go b/proxy/ss/client.go index 9cff975..b4b28a4 100644 --- a/proxy/ss/client.go +++ b/proxy/ss/client.go @@ -42,7 +42,6 @@ func (s *SS) Dial(network, addr string) (net.Conn, error) { } return c, err - } // DialUDP connects to the given address via the proxy. diff --git a/proxy/ss/internal/cipher.go b/proxy/ss/internal/cipher.go new file mode 100644 index 0000000..c951377 --- /dev/null +++ b/proxy/ss/internal/cipher.go @@ -0,0 +1,140 @@ +package internal + +import ( + "crypto/md5" + "errors" + "net" + "strings" + + "github.com/nadoo/glider/proxy/ss/internal/shadowaead" + "github.com/nadoo/glider/proxy/ss/internal/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") + +// List of AEAD ciphers: key size in bytes and constructor +var aeadList = map[string]struct { + KeySize int + New func([]byte) (shadowaead.Cipher, error) +}{ + "AEAD_AES_128_GCM": {16, shadowaead.AESGCM}, + "AEAD_AES_192_GCM": {24, shadowaead.AESGCM}, + "AEAD_AES_256_GCM": {32, shadowaead.AESGCM}, + "AEAD_CHACHA20_POLY1305": {32, shadowaead.Chacha20Poly1305}, + + // http://shadowsocks.org/en/spec/AEAD-Ciphers.html + // not listed in spec + "AEAD_XCHACHA20_POLY1305": {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) +}{ + "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}, + + // http://shadowsocks.org/en/spec/Stream-Ciphers.html + // marked as "DO NOT USE" + "CHACHA20": {32, shadowstream.ChaCha20}, + "RC4-MD5": {16, shadowstream.RC4MD5}, +} + +// 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", "NONE": + return &dummy{}, nil + case "CHACHA20-IETF-POLY1305": + name = "AEAD_CHACHA20_POLY1305" + case "XCHACHA20-IETF-POLY1305": + name = "AEAD_XCHACHA20_POLY1305" + case "AES-128-GCM": + name = "AEAD_AES_128_GCM" + case "AES-192-GCM": + name = "AEAD_AES_192_GCM" + case "AES-256-GCM": + name = "AEAD_AES_256_GCM" + } + + 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{aead}, 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{ciph}, err + } + + return nil, ErrCipherNotSupported +} + +type aeadCipher struct{ shadowaead.Cipher } + +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 } + +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/proxy/ss/internal/shadowaead/cipher.go b/proxy/ss/internal/shadowaead/cipher.go new file mode 100644 index 0000000..7c55127 --- /dev/null +++ b/proxy/ss/internal/shadowaead/cipher.go @@ -0,0 +1,92 @@ +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/proxy/ss/internal/shadowaead/conn.go b/proxy/ss/internal/shadowaead/conn.go new file mode 100644 index 0000000..2f0bb0f --- /dev/null +++ b/proxy/ss/internal/shadowaead/conn.go @@ -0,0 +1,67 @@ +package shadowaead + +import ( + "crypto/rand" + "io" + "net" +) + +type streamConn struct { + net.Conn + Cipher + r *reader + w *writer +} + +// NewConn wraps a stream-oriented net.Conn with cipher. +func NewConn(c net.Conn, ciph Cipher) net.Conn { return &streamConn{Conn: c, Cipher: ciph} } + +func (c *streamConn) 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 *streamConn) 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 *streamConn) initWriter() error { + salt := make([]byte, c.SaltSize()) + if _, err := io.ReadFull(rand.Reader, 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 *streamConn) Write(b []byte) (int, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + return c.w.Write(b) +} diff --git a/proxy/ss/internal/shadowaead/packet.go b/proxy/ss/internal/shadowaead/packet.go new file mode 100644 index 0000000..ae5f84d --- /dev/null +++ b/proxy/ss/internal/shadowaead/packet.go @@ -0,0 +1,97 @@ +package shadowaead + +import ( + "crypto/rand" + "errors" + "io" + "net" + "sync" +) + +// 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 := io.ReadFull(rand.Reader, 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 + sync.Mutex + buf []byte // write lock +} + +// NewPacketConn wraps a net.PacketConn with cipher +func NewPacketConn(c net.PacketConn, ciph Cipher) net.PacketConn { + const maxPacketSize = 64 * 1024 + return &packetConn{PacketConn: c, Cipher: ciph, buf: make([]byte, maxPacketSize)} +} + +// WriteTo encrypts b and write to addr using the embedded PacketConn. +func (c *packetConn) WriteTo(b []byte, addr net.Addr) (int, error) { + c.Lock() + defer c.Unlock() + buf, err := Pack(c.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/proxy/ss/internal/shadowaead/stream.go b/proxy/ss/internal/shadowaead/stream.go new file mode 100644 index 0000000..db1cf20 --- /dev/null +++ b/proxy/ss/internal/shadowaead/stream.go @@ -0,0 +1,151 @@ +// protocol: +// format: [encrypted payload length] [Overhead] [encrypted payload] [Overhead] +// sizes: 2 bytes, aead.Overhead() bytes, n bytes, aead.Overhead() bytes +// max(n): 0x3FFF + +package shadowaead + +import ( + "crypto/cipher" + "encoding/binary" + "io" + + "github.com/nadoo/glider/pool" +) + +const ( + lenSize = 2 + // max payload size: 16383 from ss-libev aead.c: CHUNK_SIZE_MASK + maxPayload = 0x3FFF + // buf size, shoud enough to save lenSize + aead.Overhead() + maxPayload + aead.Overhead() + bufSize = 17 << 10 +) + +type writer struct { + io.Writer + cipher.AEAD + nonce [32]byte + buf []byte +} + +// NewWriter wraps an io.Writer with AEAD encryption. +func NewWriter(w io.Writer, aead cipher.AEAD) io.Writer { return newWriter(w, aead) } + +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.GetBuffer(bufSize) + defer pool.PutBuffer(buf) + + nonce := w.nonce[:w.NonceSize()] + encLenSize := lenSize + w.Overhead() + for nw := maxPayload; n < len(p); n += nw { + if left := len(p) - n; left < maxPayload { + nw = left + } + + binary.BigEndian.PutUint16(buf[:lenSize], uint16(nw)) + w.Seal(buf[:0], nonce, buf[:lenSize], nil) + increment(nonce) + + w.Seal(buf[:encLenSize], nonce, p[n:n+nw], nil) + increment(nonce) + + if _, err = w.Writer.Write(buf[:encLenSize+nw+w.Overhead()]); err != nil { + return + } + } + return +} + +type reader struct { + io.Reader + cipher.AEAD + nonce [32]byte + buf []byte + offset int +} + +// NewReader wraps an io.Reader with AEAD decryption. +func NewReader(r io.Reader, aead cipher.AEAD) io.Reader { return newReader(r, aead) } + +func newReader(r io.Reader, aead cipher.AEAD) *reader { + return &reader{Reader: r, AEAD: aead} +} + +// NOTE: len(p) MUST >= max payload size + AEAD overhead. +func (r *reader) read(p []byte) (int, error) { + nonce := r.nonce[:r.NonceSize()] + + // read encrypted lenSize + overhead + p = p[:lenSize+r.Overhead()] + if _, err := io.ReadFull(r.Reader, p); err != nil { + return 0, err + } + + // decrypt lenSize + _, err := r.Open(p[:0], nonce, p, nil) + increment(nonce) + if err != nil { + return 0, err + } + + // get payload size + size := int(binary.BigEndian.Uint16(p[:lenSize])) + + // read encrypted payload + overhead + p = p[:size+r.Overhead()] + if _, err := io.ReadFull(r.Reader, p); err != nil { + return 0, err + } + + // decrypt payload + _, err = r.Open(p[:0], nonce, p, nil) + increment(nonce) + + if err != nil { + return 0, err + } + + return size, nil +} + +func (r *reader) Read(p []byte) (int, error) { + if r.buf == nil { + if len(p) >= maxPayload+r.Overhead() { + return r.read(p) + } + + buf := pool.GetBuffer(bufSize) + n, err := r.read(buf) + if err != nil { + pool.PutBuffer(buf) + return 0, err + } + + r.buf = buf[:n] + r.offset = 0 + } + + n := copy(p, r.buf[r.offset:]) + r.offset += n + if r.offset == len(r.buf) { + pool.PutBuffer(r.buf) + r.buf = nil + } + + return n, nil +} + +// 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 + } + } +} diff --git a/proxy/ss/internal/shadowstream/cipher.go b/proxy/ss/internal/shadowstream/cipher.go new file mode 100644 index 0000000..232ce27 --- /dev/null +++ b/proxy/ss/internal/shadowstream/cipher.go @@ -0,0 +1,134 @@ +package shadowstream + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/rc4" + "strconv" + + "github.com/aead/chacha20" + "github.com/aead/chacha20/chacha" +) + +// 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 chacha.INonceSize } +func (k chacha20ietfkey) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) } +func (k chacha20ietfkey) Encrypter(iv []byte) cipher.Stream { + ciph, err := chacha20.NewCipher(iv, k) + if err != nil { + panic(err) // should never happen + } + return ciph +} + +func Chacha20IETF(key []byte) (Cipher, error) { + if len(key) != chacha.KeySize { + return nil, KeySizeError(chacha.KeySize) + } + return chacha20ietfkey(key), nil +} + +type xchacha20key []byte + +func (k xchacha20key) IVSize() int { return chacha.XNonceSize } +func (k xchacha20key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) } +func (k xchacha20key) Encrypter(iv []byte) cipher.Stream { + ciph, err := chacha20.NewCipher(iv, k) + if err != nil { + panic(err) // should never happen + } + return ciph +} + +func Xchacha20(key []byte) (Cipher, error) { + if len(key) != chacha.KeySize { + return nil, KeySizeError(chacha.KeySize) + } + return xchacha20key(key), nil +} + +// reference: https://github.com/shadowsocks/shadowsocks-go/blob/master/shadowsocks/encrypt.go +type chacha20key []byte + +func (k chacha20key) IVSize() int { return chacha.NonceSize } +func (k chacha20key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) } +func (k chacha20key) Encrypter(iv []byte) cipher.Stream { + ciph, err := chacha20.NewCipher(iv, k) + if err != nil { + panic(err) // should never happen + } + return ciph +} + +func ChaCha20(key []byte) (Cipher, error) { + if len(key) != chacha.KeySize { + return nil, KeySizeError(chacha.KeySize) + } + return chacha20key(key), nil +} + +type rc4Md5Key []byte + +func (k rc4Md5Key) IVSize() int { return 16 } +func (k rc4Md5Key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) } +func (k rc4Md5Key) Encrypter(iv []byte) cipher.Stream { + h := md5.New() + h.Write([]byte(k)) + h.Write(iv) + rc4key := h.Sum(nil) + ciph, err := rc4.NewCipher(rc4key) + if err != nil { + panic(err) // should never happen + } + return ciph +} + +func RC4MD5(key []byte) (Cipher, error) { + return rc4Md5Key(key), nil +} diff --git a/proxy/ss/internal/shadowstream/conn.go b/proxy/ss/internal/shadowstream/conn.go new file mode 100644 index 0000000..fff77d3 --- /dev/null +++ b/proxy/ss/internal/shadowstream/conn.go @@ -0,0 +1,62 @@ +package shadowstream + +import ( + "crypto/rand" + "io" + "net" +) + +type conn struct { + net.Conn + Cipher + r *reader + w *writer +} + +// NewConn wraps a stream-oriented net.Conn with stream cipher encryption/decryption. +func NewConn(c net.Conn, ciph Cipher) net.Conn { + return &conn{Conn: c, Cipher: ciph} +} + +func (c *conn) initReader() error { + if c.r == nil { + iv := make([]byte, c.IVSize()) + if _, err := io.ReadFull(c.Conn, iv); err != nil { + return err + } + c.r = &reader{Reader: c.Conn, Stream: 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) initWriter() error { + if c.w == nil { + iv := make([]byte, c.IVSize()) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return err + } + if _, err := c.Conn.Write(iv); err != nil { + return err + } + c.w = &writer{Writer: c.Conn, Stream: 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) +} diff --git a/proxy/ss/internal/shadowstream/packet.go b/proxy/ss/internal/shadowstream/packet.go new file mode 100644 index 0000000..0defa11 --- /dev/null +++ b/proxy/ss/internal/shadowstream/packet.go @@ -0,0 +1,80 @@ +package shadowstream + +import ( + "crypto/rand" + "errors" + "io" + "net" + "sync" +) + +// 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 := io.ReadFull(rand.Reader, 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 + buf []byte + sync.Mutex // write lock +} + +// NewPacketConn wraps a net.PacketConn with stream cipher encryption/decryption. +func NewPacketConn(c net.PacketConn, ciph Cipher) net.PacketConn { + return &packetConn{PacketConn: c, Cipher: ciph, buf: make([]byte, 64*1024)} +} + +func (c *packetConn) WriteTo(b []byte, addr net.Addr) (int, error) { + c.Lock() + defer c.Unlock() + buf, err := Pack(c.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/proxy/ss/internal/shadowstream/stream.go b/proxy/ss/internal/shadowstream/stream.go new file mode 100644 index 0000000..f02f8dc --- /dev/null +++ b/proxy/ss/internal/shadowstream/stream.go @@ -0,0 +1,55 @@ +package shadowstream + +import ( + "crypto/cipher" + "io" + + "github.com/nadoo/glider/pool" +) + +const bufSize = 32 * 1024 + +type writer struct { + io.Writer + cipher.Stream +} + +// NewWriter wraps an io.Writer with stream cipher encryption. +func NewWriter(w io.Writer, s cipher.Stream) io.Writer { + return &writer{Writer: w, Stream: s} +} + +func (w *writer) Write(p []byte) (n int, err error) { + buf := pool.GetBuffer(bufSize) + defer pool.PutBuffer(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 +} + +type reader struct { + io.Reader + cipher.Stream +} + +// NewReader wraps an io.Reader with stream cipher decryption. +func NewReader(r io.Reader, s cipher.Stream) io.Reader { + return &reader{Reader: r, Stream: s} +} + +func (r *reader) Read(p []byte) (int, error) { + n, err := r.Reader.Read(p) + if err != nil { + return 0, err + } + p = p[:n] + r.XORKeyStream(p, p) + return n, nil +} diff --git a/proxy/ss/ss.go b/proxy/ss/ss.go index 81b0a63..6df3724 100644 --- a/proxy/ss/ss.go +++ b/proxy/ss/ss.go @@ -2,12 +2,10 @@ package ss import ( "net/url" - "strings" - - "github.com/nadoo/go-shadowsocks2/core" "github.com/nadoo/glider/log" "github.com/nadoo/glider/proxy" + "github.com/nadoo/glider/proxy/ss/internal" ) // SS is a base ss struct. @@ -16,7 +14,7 @@ type SS struct { proxy proxy.Proxy addr string - core.Cipher + internal.Cipher } func init() { @@ -36,7 +34,7 @@ func NewSS(s string, d proxy.Dialer, p proxy.Proxy) (*SS, error) { method := u.User.Username() pass, _ := u.User.Password() - ciph, err := core.PickCipher(method, nil, pass) + ciph, err := internal.PickCipher(method, nil, pass) if err != nil { log.Fatalf("[ss] PickCipher for '%s', error: %s", method, err) } @@ -50,8 +48,3 @@ func NewSS(s string, d proxy.Dialer, p proxy.Proxy) (*SS, error) { return ss, nil } - -// ListCipher returns all the ciphers supported. -func ListCipher() string { - return strings.Join(core.ListCipher(), " ") -} diff --git a/proxy/vmess/aead.go b/proxy/vmess/aead.go index 2309687..b12b54e 100644 --- a/proxy/vmess/aead.go +++ b/proxy/vmess/aead.go @@ -1,136 +1,127 @@ +// protocol: +// format: [data length] [data] +// sizes: 2 bytes, n bytes +// max(n): 2^14 bytes +// [data]: [encrypted payload] + [Overhead] + package vmess import ( - "bytes" "crypto/cipher" "encoding/binary" "io" + "net" + + "github.com/nadoo/glider/pool" ) type aeadWriter struct { io.Writer cipher.AEAD - nonce []byte - buf []byte + nonce [32]byte count uint16 - iv []byte } // AEADWriter returns a aead writer. func AEADWriter(w io.Writer, aead cipher.AEAD, iv []byte) io.Writer { - return &aeadWriter{ - Writer: w, - AEAD: aead, - buf: make([]byte, lenSize+chunkSize), - nonce: make([]byte, aead.NonceSize()), - count: 0, - iv: iv, - } + aw := &aeadWriter{Writer: w, AEAD: aead} + copy(aw.nonce[2:], iv[2:12]) + return aw } -func (w *aeadWriter) Write(b []byte) (int, error) { - n, err := w.ReadFrom(bytes.NewBuffer(b)) - return int(n), err -} +func (w *aeadWriter) Write(b []byte) (n int, err error) { + buf := pool.GetBuffer(chunkSize) + defer pool.PutBuffer(buf) -func (w *aeadWriter) ReadFrom(r io.Reader) (n int64, err error) { - for { - buf := w.buf - payloadBuf := buf[lenSize : lenSize+chunkSize-w.Overhead()] + var lenBuf [lenSize]byte + var writeLen, dataLen int - nr, er := r.Read(payloadBuf) - if nr > 0 { - n += int64(nr) - buf = buf[:lenSize+nr+w.Overhead()] - payloadBuf = payloadBuf[:nr] - binary.BigEndian.PutUint16(buf[:lenSize], uint16(nr+w.Overhead())) - - binary.BigEndian.PutUint16(w.nonce[:2], w.count) - copy(w.nonce[2:], w.iv[2:12]) - - w.Seal(payloadBuf[:0], w.nonce, payloadBuf, nil) - w.count++ - - _, ew := w.Writer.Write(buf) - if ew != nil { - err = ew - break - } + nonce := w.nonce[:w.NonceSize()] + for left := len(b); left != 0; { + writeLen = left + w.Overhead() + if writeLen > chunkSize { + writeLen = chunkSize } + dataLen = writeLen - w.Overhead() - if er != nil { - if er != io.EOF { // ignore EOF as per io.ReaderFrom contract - err = er - } + binary.BigEndian.PutUint16(lenBuf[:], uint16(writeLen)) + binary.BigEndian.PutUint16(nonce[:2], w.count) + + w.Seal(buf[:0], nonce, b[n:n+dataLen], nil) + w.count++ + + if _, err = (&net.Buffers{lenBuf[:], buf[:writeLen]}).WriteTo(w.Writer); err != nil { break } + + n += dataLen + left -= dataLen } - return n, err + return } type aeadReader struct { io.Reader cipher.AEAD - nonce []byte - buf []byte - leftover []byte - count uint16 - iv []byte + nonce [32]byte + count uint16 + buf []byte + offset int } // AEADReader returns a aead reader. func AEADReader(r io.Reader, aead cipher.AEAD, iv []byte) io.Reader { - return &aeadReader{ - Reader: r, - AEAD: aead, - buf: make([]byte, lenSize+chunkSize), - nonce: make([]byte, aead.NonceSize()), - count: 0, - iv: iv, - } + ar := &aeadReader{Reader: r, AEAD: aead} + copy(ar.nonce[2:], iv[2:12]) + return ar } -func (r *aeadReader) Read(b []byte) (int, error) { - if len(r.leftover) > 0 { - n := copy(b, r.leftover) - r.leftover = r.leftover[n:] - return n, nil - } - - // get length - _, err := io.ReadFull(r.Reader, r.buf[:lenSize]) - if err != nil { +func (r *aeadReader) read(p []byte) (int, error) { + if _, err := io.ReadFull(r.Reader, p[:lenSize]); err != nil { return 0, err } - // if length == 0, then this is the end - l := binary.BigEndian.Uint16(r.buf[:lenSize]) - if l == 0 { - return 0, nil - } - - // get payload - buf := r.buf[:l] - _, err = io.ReadFull(r.Reader, buf) - if err != nil { + size := int(binary.BigEndian.Uint16(p[:lenSize])) + p = p[:size] + if _, err := io.ReadFull(r.Reader, p); err != nil { 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, buf, nil) + _, err := r.Open(p[:0], r.nonce[:r.NonceSize()], p, nil) r.count++ + if err != nil { return 0, err } - dataLen := int(l) - r.Overhead() - m := copy(b, r.buf[:dataLen]) - if m < int(dataLen) { - r.leftover = r.buf[m:dataLen] + return size - r.Overhead(), nil +} + +func (r *aeadReader) Read(p []byte) (int, error) { + if r.buf == nil { + if len(p) >= chunkSize { + return r.read(p) + } + + buf := pool.GetBuffer(chunkSize) + n, err := r.read(buf) + if err != nil || n == 0 { + pool.PutBuffer(buf) + return 0, err + } + + r.buf = buf[:n] + r.offset = 0 } - return m, err + n := copy(p, r.buf[r.offset:]) + r.offset += n + if r.offset == len(r.buf) { + pool.PutBuffer(r.buf) + r.buf = nil + } + + return n, nil } diff --git a/proxy/vmess/chunk.go b/proxy/vmess/chunk.go index 797df17..0d8ea30 100644 --- a/proxy/vmess/chunk.go +++ b/proxy/vmess/chunk.go @@ -3,17 +3,17 @@ package vmess import ( "encoding/binary" "io" - - "github.com/nadoo/glider/pool" + "net" ) const ( lenSize = 2 - chunkSize = 1 << 14 // 16384 + chunkSize = 16 << 10 ) type chunkedWriter struct { io.Writer + buf [lenSize]byte } // ChunkedWriter returns a chunked writer. @@ -21,28 +21,22 @@ func ChunkedWriter(w io.Writer) io.Writer { return &chunkedWriter{Writer: w} } -func (w *chunkedWriter) Write(b []byte) (n int, err error) { - buf := pool.GetBuffer(lenSize + chunkSize) - defer pool.PutBuffer(buf) - - for left := len(b); left != 0; { - writeLen := left - if writeLen > chunkSize { - writeLen = chunkSize +func (w *chunkedWriter) Write(p []byte) (n int, err error) { + var dataLen int + for left := len(p); left != 0; { + dataLen = left + if dataLen > chunkSize { + dataLen = chunkSize } - copy(buf[lenSize:], b[n:n+writeLen]) - binary.BigEndian.PutUint16(buf[:lenSize], uint16(writeLen)) - - _, err = w.Writer.Write(buf[:lenSize+writeLen]) - if err != nil { + binary.BigEndian.PutUint16(w.buf[:], uint16(dataLen)) + if _, err = (&net.Buffers{w.buf[:], p[n : n+dataLen]}).WriteTo(w.Writer); err != nil { break } - n += writeLen - left -= writeLen + n += dataLen + left -= dataLen } - return } @@ -57,7 +51,7 @@ func ChunkedReader(r io.Reader) io.Reader { return &chunkedReader{Reader: r} } -func (r *chunkedReader) Read(b []byte) (int, error) { +func (r *chunkedReader) Read(p []byte) (int, error) { if r.left == 0 { // get length _, err := io.ReadFull(r.Reader, r.buf[:lenSize]) @@ -72,12 +66,12 @@ func (r *chunkedReader) Read(b []byte) (int, error) { } } - readLen := len(b) + readLen := len(p) if readLen > r.left { readLen = r.left } - n, err := r.Reader.Read(b[:readLen]) + n, err := r.Reader.Read(p[:readLen]) if err != nil { return 0, err }