Chore: embed shadowsocks2
This commit is contained in:
parent
3360839fe3
commit
09d49bac95
19 changed files with 1045 additions and 21 deletions
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
1
go.mod
1
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
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=
|
||||
|
|
5
transport/shadowsocks/README.md
Normal file
5
transport/shadowsocks/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
## Embedded go-shadowsocks2
|
||||
|
||||
from https://github.com/Dreamacro/go-shadowsocks2
|
||||
|
||||
origin https://github.com/riobard/go-shadowsocks2
|
164
transport/shadowsocks/core/cipher.go
Normal file
164
transport/shadowsocks/core/cipher.go
Normal file
|
@ -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]
|
||||
}
|
94
transport/shadowsocks/shadowaead/cipher.go
Normal file
94
transport/shadowsocks/shadowaead/cipher.go
Normal file
|
@ -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
|
||||
}
|
95
transport/shadowsocks/shadowaead/packet.go
Normal file
95
transport/shadowsocks/shadowaead/packet.go
Normal file
|
@ -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
|
||||
}
|
285
transport/shadowsocks/shadowaead/stream.go
Normal file
285
transport/shadowsocks/shadowaead/stream.go
Normal file
|
@ -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)
|
||||
}
|
116
transport/shadowsocks/shadowstream/cipher.go
Normal file
116
transport/shadowsocks/shadowstream/cipher.go
Normal file
|
@ -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
|
||||
}
|
79
transport/shadowsocks/shadowstream/packet.go
Normal file
79
transport/shadowsocks/shadowstream/packet.go
Normal file
|
@ -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
|
||||
}
|
197
transport/shadowsocks/shadowstream/stream.go
Normal file
197
transport/shadowsocks/shadowstream/stream.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue