Feature: experimental support snell
This commit is contained in:
parent
1d17ab49da
commit
204ff1a356
6 changed files with 277 additions and 31 deletions
115
README.md
115
README.md
|
@ -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/
|
||||||
|
|
71
adapters/outbound/snell.go
Normal file
71
adapters/outbound/snell.go
Normal 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
21
component/snell/cipher.go
Normal 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
91
component/snell/snell.go
Normal 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)}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue