From 204ff1a356c6ef921173c35453542be33459e417 Mon Sep 17 00:00:00 2001 From: gVisor bot Date: Wed, 9 Oct 2019 18:46:23 +0800 Subject: [PATCH] Feature: experimental support snell --- README.md | 115 +++++++++++++++++++++++++++---------- adapters/outbound/snell.go | 71 +++++++++++++++++++++++ component/snell/cipher.go | 21 +++++++ component/snell/snell.go | 91 +++++++++++++++++++++++++++++ config/config.go | 7 +++ constant/adapters.go | 3 + 6 files changed, 277 insertions(+), 31 deletions(-) create mode 100644 adapters/outbound/snell.go create mode 100644 component/snell/cipher.go create mode 100644 component/snell/snell.go diff --git a/README.md b/README.md index 6f60bcfe..ab44890f 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,8 @@ For example, you can use the current directory as the configuration directory: $ clash -d . ``` -Below is an example configuration file: +
+ This is an example configuration file ```yml # port of HTTP @@ -91,7 +92,7 @@ allow-lan: false # "*": bind all IP addresses # 192.168.122.11: bind a single IPv4 address # "[aaaa::a8aa:ff:fe09:57d8]": bind a single IPv6 address -bind-address: "*" +# bind-address: "*" # Rule / Global/ Direct (default is Rule) mode: Rule @@ -151,9 +152,15 @@ Proxy: # aes-128-ctr aes-192-ctr aes-256-ctr # rc4-md5 chacha20 chacha20-ietf xchacha20 # chacha20-ietf-poly1305 xchacha20-ietf-poly1305 -- { name: "ss1", type: ss, server: server, port: 443, cipher: chacha20-ietf-poly1305, password: "password", udp: true } +- name: "ss1" + type: ss + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + # udp: true -# old obfs configuration remove after prerelease +# old obfs configuration format remove after prerelease - name: "ss2" type: ss server: server @@ -184,47 +191,92 @@ Proxy: # vmess # cipher support auto/aes-128-gcm/chacha20-poly1305/none -- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto } -# with tls -- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true } -# with tls and skip-cert-verify -- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true, skip-cert-verify: true } -# with ws-path and ws-headers -- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path, ws-headers: { Host: v2ray.com } } -# with ws + tls -- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path, tls: true } +- name: "vmess" + type: vmess + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + # udp: true + # tls: true + # skip-cert-verify: true + # network: ws + # ws-path: /path + # ws-headers: + # Host: v2ray.com # socks5 -- { name: "socks", type: socks5, server: server, port: 443 } -# socks5 with authentication -- { name: "socks", type: socks5, server: server, port: 443, username: "username", password: "password" } -# with tls -- { name: "socks", type: socks5, server: server, port: 443, tls: true } -# with tls and skip-cert-verify -- { name: "socks", type: socks5, server: server, port: 443, tls: true, skip-cert-verify: true } +- name: "socks" + type: socks5 + server: server + port: 443 + # username: username + # password: password + # tls: true + # skip-cert-verify: true + # udp: true # http -- { name: "http", type: http, server: server, port: 443 } -# http with authentication -- { name: "http", type: http, server: server, port: 443, username: "username", password: "password" } -# with tls (https) -- { name: "http", type: http, server: server, port: 443, tls: true } -# with tls (https) and skip-cert-verify -- { name: "http", type: http, server: server, port: 443, tls: true, skip-cert-verify: true } +- name: "http" + type: http + server: server + port: 443 + # username: username + # password: password + # tls: true # https + # skip-cert-verify: true + +# snell +- name: "snell" + type: snell + server: server + port: 44046 + psk: yourpsk + # obfs-opts: + # mode: http # or tls + # host: bing.com Proxy Group: # url-test select which proxy will be used by benchmarking speed to a URL. -- { name: "auto", type: url-test, proxies: ["ss1", "ss2", "vmess1"], url: "http://www.gstatic.com/generate_204", interval: 300 } +- name: "auto" + type: url-test + proxies: + - ss1 + - ss2 + - vmess1 + url: 'http://www.gstatic.com/generate_204' + interval: 300 # fallback select an available policy by priority. The availability is tested by accessing an URL, just like an auto url-test group. -- { name: "fallback-auto", type: fallback, proxies: ["ss1", "ss2", "vmess1"], url: "http://www.gstatic.com/generate_204", interval: 300 } +- name: "fallback-auto" + type: fallback + proxies: + - ss1 + - ss2 + - vmess1 + url: 'http://www.gstatic.com/generate_204' + interval: 300 # load-balance: The request of the same eTLD will be dial on the same proxy. -- { name: "load-balance", type: load-balance, proxies: ["ss1", "ss2", "vmess1"], url: "http://www.gstatic.com/generate_204", interval: 300 } +- name: "load-balance" + type: load-balance + proxies: + - ss1 + - ss2 + - vmess1 + url: 'http://www.gstatic.com/generate_204' + interval: 300 # select is used for selecting proxy or proxy group # you can use RESTful API to switch proxy, is recommended for use in GUI. -- { name: "Proxy", type: select, proxies: ["ss1", "ss2", "vmess1", "auto"] } +- name: Proxy + type: select + proxies: + - ss1 + - ss2 + - vmess1 + - auto Rule: - DOMAIN-SUFFIX,google.com,auto @@ -241,6 +293,7 @@ Rule: # you also can use `FINAL,Proxy` or `FINAL,,Proxy` now - MATCH,auto ``` +
## Documentations https://clash.gitbook.io/ diff --git a/adapters/outbound/snell.go b/adapters/outbound/snell.go new file mode 100644 index 00000000..b4131199 --- /dev/null +++ b/adapters/outbound/snell.go @@ -0,0 +1,71 @@ +package adapters + +import ( + "fmt" + "net" + "strconv" + + "github.com/Dreamacro/clash/common/structure" + obfs "github.com/Dreamacro/clash/component/simple-obfs" + "github.com/Dreamacro/clash/component/snell" + C "github.com/Dreamacro/clash/constant" +) + +type Snell struct { + *Base + server string + psk []byte + obfsOption *simpleObfsOption +} + +type SnellOption struct { + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + Psk string `proxy:"psk"` + ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"` +} + +func (s *Snell) Dial(metadata *C.Metadata) (C.Conn, error) { + c, err := dialTimeout("tcp", s.server, tcpTimeout) + if err != nil { + return nil, fmt.Errorf("%s connect error: %s", s.server, err.Error()) + } + tcpKeepAlive(c) + switch s.obfsOption.Mode { + case "tls": + c = obfs.NewTLSObfs(c, s.obfsOption.Host) + case "http": + _, port, _ := net.SplitHostPort(s.server) + c = obfs.NewHTTPObfs(c, s.obfsOption.Host, port) + } + c = snell.StreamConn(c, s.psk) + port, _ := strconv.Atoi(metadata.DstPort) + err = snell.WriteHeader(c, metadata.String(), uint(port)) + return newConn(c, s), err +} + +func NewSnell(option SnellOption) (*Snell, error) { + server := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + psk := []byte(option.Psk) + + decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) + obfsOption := &simpleObfsOption{Host: "bing.com"} + if err := decoder.Decode(option.ObfsOpts, obfsOption); err != nil { + return nil, fmt.Errorf("snell %s initialize obfs error: %s", server, err.Error()) + } + + if obfsOption.Mode != "tls" && obfsOption.Mode != "http" { + return nil, fmt.Errorf("snell %s obfs mode error: %s", server, obfsOption.Mode) + } + + return &Snell{ + Base: &Base{ + name: option.Name, + tp: C.Snell, + }, + server: server, + psk: psk, + obfsOption: obfsOption, + }, nil +} diff --git a/component/snell/cipher.go b/component/snell/cipher.go new file mode 100644 index 00000000..f66f0801 --- /dev/null +++ b/component/snell/cipher.go @@ -0,0 +1,21 @@ +package snell + +import ( + "crypto/cipher" + + "golang.org/x/crypto/argon2" +) + +type snellCipher struct { + psk []byte + makeAEAD func(key []byte) (cipher.AEAD, error) +} + +func (sc *snellCipher) KeySize() int { return 32 } +func (sc *snellCipher) SaltSize() int { return 16 } +func (sc *snellCipher) Encrypter(salt []byte) (cipher.AEAD, error) { + return sc.makeAEAD(argon2.IDKey(sc.psk, salt, 3, 8, 1, uint32(sc.KeySize()))) +} +func (sc *snellCipher) Decrypter(salt []byte) (cipher.AEAD, error) { + return sc.makeAEAD(argon2.IDKey(sc.psk, salt, 3, 8, 1, uint32(sc.KeySize()))) +} diff --git a/component/snell/snell.go b/component/snell/snell.go new file mode 100644 index 00000000..6b40b383 --- /dev/null +++ b/component/snell/snell.go @@ -0,0 +1,91 @@ +package snell + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + "net" + "sync" + + "github.com/Dreamacro/go-shadowsocks2/shadowaead" + "golang.org/x/crypto/chacha20poly1305" +) + +const ( + CommandPing byte = 0 + CommandConnect byte = 1 + + CommandTunnel byte = 0 + CommandError byte = 2 + + Version byte = 1 +) + +var ( + bufferPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} +) + +type Snell struct { + net.Conn + buffer [1]byte + reply bool +} + +func (s *Snell) Read(b []byte) (int, error) { + if s.reply { + return s.Conn.Read(b) + } + + s.reply = true + if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil { + return 0, err + } + + if s.buffer[0] == CommandTunnel { + return s.Conn.Read(b) + } else if s.buffer[0] != CommandError { + return 0, errors.New("Command not support") + } + + // CommandError + if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil { + return 0, err + } + + length := int(s.buffer[0]) + msg := make([]byte, length) + + if _, err := io.ReadFull(s.Conn, msg); err != nil { + return 0, err + } + + return 0, errors.New(string(msg)) +} + +func WriteHeader(conn net.Conn, host string, port uint) error { + buf := bufferPool.Get().(*bytes.Buffer) + buf.Reset() + defer bufferPool.Put(buf) + buf.WriteByte(Version) + buf.WriteByte(CommandConnect) + + // clientID length & id + buf.WriteByte(0) + + // host & port + buf.WriteByte(uint8(len(host))) + buf.WriteString(host) + binary.Write(buf, binary.BigEndian, uint16(port)) + + if _, err := conn.Write(buf.Bytes()); err != nil { + return err + } + + return nil +} + +func StreamConn(conn net.Conn, psk []byte) net.Conn { + cipher := &snellCipher{psk, chacha20poly1305.New} + return &Snell{Conn: shadowaead.NewConn(conn, cipher)} +} diff --git a/config/config.go b/config/config.go index 16445111..7a5bce4a 100644 --- a/config/config.go +++ b/config/config.go @@ -300,6 +300,13 @@ func parseProxies(cfg *rawConfig) (map[string]C.Proxy, error) { break } proxy, err = adapters.NewVmess(*vmessOption) + case "snell": + snellOption := &adapters.SnellOption{} + err = decoder.Decode(mapping, snellOption) + if err != nil { + break + } + proxy, err = adapters.NewSnell(*snellOption) default: return nil, fmt.Errorf("Unsupport proxy type: %s", proxyType) } diff --git a/constant/adapters.go b/constant/adapters.go index 4593fed0..2e155ac8 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -14,6 +14,7 @@ const ( Reject Selector Shadowsocks + Snell Socks5 Http URLTest @@ -92,6 +93,8 @@ func (at AdapterType) String() string { return "Selector" case Shadowsocks: return "Shadowsocks" + case Snell: + return "Snell" case Socks5: return "Socks5" case Http: