Feature: experimental support snell

This commit is contained in:
Dreamacro 2019-10-09 18:46:23 +08:00
parent 54386ccda3
commit 06c9dfdb80
6 changed files with 277 additions and 31 deletions

115
README.md
View file

@ -73,7 +73,8 @@ For example, you can use the current directory as the configuration directory:
$ clash -d . $ clash -d .
``` ```
Below is an example configuration file: <details>
<summary>This is an example configuration file</summary>
```yml ```yml
# port of HTTP # port of HTTP
@ -91,7 +92,7 @@ allow-lan: false
# "*": bind all IP addresses # "*": bind all IP addresses
# 192.168.122.11: bind a single IPv4 address # 192.168.122.11: bind a single IPv4 address
# "[aaaa::a8aa:ff:fe09:57d8]": bind a single IPv6 address # "[aaaa::a8aa:ff:fe09:57d8]": bind a single IPv6 address
bind-address: "*" # bind-address: "*"
# Rule / Global/ Direct (default is Rule) # Rule / Global/ Direct (default is Rule)
mode: Rule mode: Rule
@ -151,9 +152,15 @@ Proxy:
# aes-128-ctr aes-192-ctr aes-256-ctr # aes-128-ctr aes-192-ctr aes-256-ctr
# rc4-md5 chacha20 chacha20-ietf xchacha20 # rc4-md5 chacha20 chacha20-ietf xchacha20
# chacha20-ietf-poly1305 xchacha20-ietf-poly1305 # 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" - name: "ss2"
type: ss type: ss
server: server server: server
@ -184,47 +191,92 @@ Proxy:
# vmess # vmess
# cipher support auto/aes-128-gcm/chacha20-poly1305/none # cipher support auto/aes-128-gcm/chacha20-poly1305/none
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto } - name: "vmess"
# with tls type: vmess
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true } server: server
# with tls and skip-cert-verify port: 443
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true, skip-cert-verify: true } uuid: uuid
# with ws-path and ws-headers alterId: 32
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path, ws-headers: { Host: v2ray.com } } cipher: auto
# with ws + tls # udp: true
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path, tls: true } # tls: true
# skip-cert-verify: true
# network: ws
# ws-path: /path
# ws-headers:
# Host: v2ray.com
# socks5 # socks5
- { name: "socks", type: socks5, server: server, port: 443 } - name: "socks"
# socks5 with authentication type: socks5
- { name: "socks", type: socks5, server: server, port: 443, username: "username", password: "password" } server: server
# with tls port: 443
- { name: "socks", type: socks5, server: server, port: 443, tls: true } # username: username
# with tls and skip-cert-verify # password: password
- { name: "socks", type: socks5, server: server, port: 443, tls: true, skip-cert-verify: true } # tls: true
# skip-cert-verify: true
# udp: true
# http # http
- { name: "http", type: http, server: server, port: 443 } - name: "http"
# http with authentication type: http
- { name: "http", type: http, server: server, port: 443, username: "username", password: "password" } server: server
# with tls (https) port: 443
- { name: "http", type: http, server: server, port: 443, tls: true } # username: username
# with tls (https) and skip-cert-verify # password: password
- { name: "http", type: http, server: server, port: 443, tls: true, skip-cert-verify: true } # 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: Proxy Group:
# url-test select which proxy will be used by benchmarking speed to a URL. # 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. # 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. # 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 # select is used for selecting proxy or proxy group
# you can use RESTful API to switch proxy, is recommended for use in GUI. # 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: Rule:
- DOMAIN-SUFFIX,google.com,auto - DOMAIN-SUFFIX,google.com,auto
@ -241,6 +293,7 @@ Rule:
# you also can use `FINAL,Proxy` or `FINAL,,Proxy` now # you also can use `FINAL,Proxy` or `FINAL,,Proxy` now
- MATCH,auto - MATCH,auto
``` ```
</details>
## Documentations ## Documentations
https://clash.gitbook.io/ https://clash.gitbook.io/

View file

@ -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
}

21
component/snell/cipher.go Normal file
View file

@ -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())))
}

91
component/snell/snell.go Normal file
View file

@ -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)}
}

View file

@ -300,6 +300,13 @@ func parseProxies(cfg *rawConfig) (map[string]C.Proxy, error) {
break break
} }
proxy, err = adapters.NewVmess(*vmessOption) 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: default:
return nil, fmt.Errorf("Unsupport proxy type: %s", proxyType) return nil, fmt.Errorf("Unsupport proxy type: %s", proxyType)
} }

View file

@ -14,6 +14,7 @@ const (
Reject Reject
Selector Selector
Shadowsocks Shadowsocks
Snell
Socks5 Socks5
Http Http
URLTest URLTest
@ -92,6 +93,8 @@ func (at AdapterType) String() string {
return "Selector" return "Selector"
case Shadowsocks: case Shadowsocks:
return "Shadowsocks" return "Shadowsocks"
case Snell:
return "Snell"
case Socks5: case Socks5:
return "Socks5" return "Socks5"
case Http: case Http: