Feature: add v2ray-plugin
This commit is contained in:
parent
3a3fad3d32
commit
2500169447
6 changed files with 332 additions and 44 deletions
|
@ -2,12 +2,15 @@ package adapters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
obfs "github.com/Dreamacro/clash/component/simple-obfs"
|
obfs "github.com/Dreamacro/clash/component/simple-obfs"
|
||||||
|
v2rayObfs "github.com/Dreamacro/clash/component/v2ray-plugin"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
|
||||||
"github.com/Dreamacro/go-shadowsocks2/core"
|
"github.com/Dreamacro/go-shadowsocks2/core"
|
||||||
|
@ -16,34 +19,60 @@ import (
|
||||||
|
|
||||||
type ShadowSocks struct {
|
type ShadowSocks struct {
|
||||||
*Base
|
*Base
|
||||||
server string
|
server string
|
||||||
obfs string
|
cipher core.Cipher
|
||||||
obfsHost string
|
|
||||||
cipher core.Cipher
|
// obfs
|
||||||
|
obfsMode string
|
||||||
|
obfsOption *simpleObfsOption
|
||||||
|
wsOption *v2rayObfs.WebsocketOption
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShadowSocksOption struct {
|
type ShadowSocksOption struct {
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
Password string `proxy:"password"`
|
Password string `proxy:"password"`
|
||||||
Cipher string `proxy:"cipher"`
|
Cipher string `proxy:"cipher"`
|
||||||
|
Plugin string `proxy:"plugin,omitempty"`
|
||||||
|
PluginOpts map[string]interface{} `proxy:"plugin-opts,omitempty"`
|
||||||
|
|
||||||
|
// deprecated when bump to 1.0
|
||||||
Obfs string `proxy:"obfs,omitempty"`
|
Obfs string `proxy:"obfs,omitempty"`
|
||||||
ObfsHost string `proxy:"obfs-host,omitempty"`
|
ObfsHost string `proxy:"obfs-host,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type simpleObfsOption struct {
|
||||||
|
Mode string `obfs:"mode"`
|
||||||
|
Host string `obfs:"host,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type v2rayObfsOption struct {
|
||||||
|
Mode string `obfs:"mode"`
|
||||||
|
Host string `obfs:"host,omitempty"`
|
||||||
|
Path string `obfs:"path,omitempty"`
|
||||||
|
TLS bool `obfs:"tls,omitempty"`
|
||||||
|
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func (ss *ShadowSocks) Generator(metadata *C.Metadata) (net.Conn, error) {
|
func (ss *ShadowSocks) Generator(metadata *C.Metadata) (net.Conn, error) {
|
||||||
c, err := net.DialTimeout("tcp", ss.server, tcpTimeout)
|
c, err := net.DialTimeout("tcp", ss.server, tcpTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error", ss.server)
|
return nil, fmt.Errorf("%s connect error: %s", ss.server, err.Error())
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
switch ss.obfs {
|
switch ss.obfsMode {
|
||||||
case "tls":
|
case "tls":
|
||||||
c = obfs.NewTLSObfs(c, ss.obfsHost)
|
c = obfs.NewTLSObfs(c, ss.obfsOption.Host)
|
||||||
case "http":
|
case "http":
|
||||||
_, port, _ := net.SplitHostPort(ss.server)
|
_, port, _ := net.SplitHostPort(ss.server)
|
||||||
c = obfs.NewHTTPObfs(c, ss.obfsHost, port)
|
c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port)
|
||||||
|
case "websocket":
|
||||||
|
var err error
|
||||||
|
c, err = v2rayObfs.NewWebsocketObfs(c, ss.wsOption)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %s", ss.server, err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
c = ss.cipher.StreamConn(c)
|
c = ss.cipher.StreamConn(c)
|
||||||
_, err = c.Write(serializesSocksAddr(metadata))
|
_, err = c.Write(serializesSocksAddr(metadata))
|
||||||
|
@ -65,10 +94,49 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||||
return nil, fmt.Errorf("ss %s initialize error: %s", server, err.Error())
|
return nil, fmt.Errorf("ss %s initialize error: %s", server, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
obfs := option.Obfs
|
var wsOption *v2rayObfs.WebsocketOption
|
||||||
obfsHost := "bing.com"
|
var obfsOption *simpleObfsOption
|
||||||
if option.ObfsHost != "" {
|
obfsMode := ""
|
||||||
obfsHost = option.ObfsHost
|
|
||||||
|
// forward compatibility before 1.0
|
||||||
|
if option.Obfs != "" {
|
||||||
|
obfsMode = option.Obfs
|
||||||
|
obfsOption = &simpleObfsOption{
|
||||||
|
Host: "bing.com",
|
||||||
|
}
|
||||||
|
if option.ObfsHost != "" {
|
||||||
|
obfsOption.Host = option.ObfsHost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
||||||
|
if option.Plugin == "obfs" {
|
||||||
|
opts := simpleObfsOption{Host: "bing.com"}
|
||||||
|
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
|
||||||
|
return nil, fmt.Errorf("ss %s initialize obfs error: %s", server, err.Error())
|
||||||
|
}
|
||||||
|
obfsMode = opts.Mode
|
||||||
|
obfsOption = &opts
|
||||||
|
} else if option.Plugin == "v2ray-plugin" {
|
||||||
|
opts := v2rayObfsOption{Host: "bing.com"}
|
||||||
|
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
|
||||||
|
return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %s", server, err.Error())
|
||||||
|
}
|
||||||
|
obfsMode = opts.Mode
|
||||||
|
var tlsConfig *tls.Config
|
||||||
|
if opts.TLS {
|
||||||
|
tlsConfig = &tls.Config{
|
||||||
|
ServerName: opts.Host,
|
||||||
|
InsecureSkipVerify: opts.SkipCertVerify,
|
||||||
|
ClientSessionCache: getClientSessionCache(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wsOption = &v2rayObfs.WebsocketOption{
|
||||||
|
Host: opts.Host,
|
||||||
|
Path: opts.Path,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ShadowSocks{
|
return &ShadowSocks{
|
||||||
|
@ -76,10 +144,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
tp: C.Shadowsocks,
|
tp: C.Shadowsocks,
|
||||||
},
|
},
|
||||||
server: server,
|
server: server,
|
||||||
cipher: ciph,
|
cipher: ciph,
|
||||||
obfs: obfs,
|
|
||||||
obfsHost: obfsHost,
|
obfsMode: obfsMode,
|
||||||
|
wsOption: wsOption,
|
||||||
|
obfsOption: obfsOption,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,8 @@ func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error
|
||||||
return d.decodeSlice(name, data, val)
|
return d.decodeSlice(name, data, val)
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
return d.decodeMap(name, data, val)
|
return d.decodeMap(name, data, val)
|
||||||
|
case reflect.Interface:
|
||||||
|
return d.setInterface(name, data, val)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("type %s not support", val.Kind().String())
|
return fmt.Errorf("type %s not support", val.Kind().String())
|
||||||
}
|
}
|
||||||
|
@ -229,3 +231,9 @@ func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val refle
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) setInterface(name string, data interface{}, val reflect.Value) (err error) {
|
||||||
|
dataVal := reflect.ValueOf(data)
|
||||||
|
val.Set(dataVal)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
173
component/v2ray-plugin/mux.go
Normal file
173
component/v2ray-plugin/mux.go
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
package obfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionStatus = byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
SessionStatusNew SessionStatus = 0x01
|
||||||
|
SessionStatusKeep SessionStatus = 0x02
|
||||||
|
SessionStatusEnd SessionStatus = 0x03
|
||||||
|
SessionStatusKeepAlive SessionStatus = 0x04
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
OptionNone = byte(0x00)
|
||||||
|
OptionData = byte(0x01)
|
||||||
|
OptionError = byte(0x02)
|
||||||
|
)
|
||||||
|
|
||||||
|
type MuxOption struct {
|
||||||
|
ID [2]byte
|
||||||
|
Port uint16
|
||||||
|
Host string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mux is an mux-compatible client for v2ray-plugin, not a complete implementation
|
||||||
|
type Mux struct {
|
||||||
|
net.Conn
|
||||||
|
buf bytes.Buffer
|
||||||
|
id [2]byte
|
||||||
|
length [2]byte
|
||||||
|
status [2]byte
|
||||||
|
otb []byte
|
||||||
|
remain int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mux) Read(b []byte) (int, error) {
|
||||||
|
if m.remain != 0 {
|
||||||
|
length := m.remain
|
||||||
|
if len(b) < m.remain {
|
||||||
|
length = len(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := m.Conn.Read(b[:length])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
m.remain = m.remain - n
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, err := io.ReadFull(m.Conn, m.length[:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
length := binary.BigEndian.Uint16(m.length[:])
|
||||||
|
if length > 512 {
|
||||||
|
return 0, errors.New("invalid metalen")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.ReadFull(m.Conn, m.id[:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = m.Conn.Read(m.status[:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
opcode := m.status[0]
|
||||||
|
if opcode == SessionStatusKeepAlive {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := m.status[1]
|
||||||
|
|
||||||
|
if opts != OptionData {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.ReadFull(m.Conn, m.length[:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
dataLen := int(binary.BigEndian.Uint16(m.length[:]))
|
||||||
|
m.remain = dataLen
|
||||||
|
if dataLen > len(b) {
|
||||||
|
dataLen = len(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := m.Conn.Read(b[:dataLen])
|
||||||
|
m.remain -= n
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mux) Write(b []byte) (int, error) {
|
||||||
|
if m.otb != nil {
|
||||||
|
// create a sub connection
|
||||||
|
if _, err := m.Conn.Write(m.otb); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
m.otb = nil
|
||||||
|
}
|
||||||
|
m.buf.Reset()
|
||||||
|
binary.Write(&m.buf, binary.BigEndian, uint16(4))
|
||||||
|
m.buf.Write(m.id[:])
|
||||||
|
m.buf.WriteByte(SessionStatusKeep)
|
||||||
|
m.buf.WriteByte(OptionData)
|
||||||
|
binary.Write(&m.buf, binary.BigEndian, uint16(len(b)))
|
||||||
|
m.buf.Write(b)
|
||||||
|
|
||||||
|
return m.Conn.Write(m.buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mux) Close() error {
|
||||||
|
_, err := m.Conn.Write([]byte{0x0, 0x4, m.id[0], m.id[1], SessionStatusEnd, OptionNone})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMux(conn net.Conn, option MuxOption) *Mux {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
// fill empty length
|
||||||
|
buf.Write([]byte{0x0, 0x0})
|
||||||
|
buf.Write(option.ID[:])
|
||||||
|
buf.WriteByte(SessionStatusNew)
|
||||||
|
buf.WriteByte(OptionNone)
|
||||||
|
|
||||||
|
// tcp
|
||||||
|
netType := byte(0x1)
|
||||||
|
if option.Type == "udp" {
|
||||||
|
netType = byte(0x2)
|
||||||
|
}
|
||||||
|
buf.WriteByte(netType)
|
||||||
|
|
||||||
|
// port
|
||||||
|
binary.Write(buf, binary.BigEndian, option.Port)
|
||||||
|
|
||||||
|
// address
|
||||||
|
ip := net.ParseIP(option.Host)
|
||||||
|
if ip == nil {
|
||||||
|
buf.WriteByte(0x2)
|
||||||
|
buf.WriteString(option.Host)
|
||||||
|
} else if ipv4 := ip.To4(); ipv4 != nil {
|
||||||
|
buf.WriteByte(0x1)
|
||||||
|
buf.Write(ipv4)
|
||||||
|
} else {
|
||||||
|
buf.WriteByte(0x3)
|
||||||
|
buf.Write(ip.To16())
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := buf.Bytes()
|
||||||
|
binary.BigEndian.PutUint16(metadata[:2], uint16(len(metadata)-2))
|
||||||
|
|
||||||
|
return &Mux{
|
||||||
|
Conn: conn,
|
||||||
|
id: option.ID,
|
||||||
|
otb: metadata,
|
||||||
|
}
|
||||||
|
}
|
37
component/v2ray-plugin/websocket.go
Normal file
37
component/v2ray-plugin/websocket.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package obfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/vmess"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebsocketOption is options of websocket obfs
|
||||||
|
type WebsocketOption struct {
|
||||||
|
Host string
|
||||||
|
Path string
|
||||||
|
TLSConfig *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWebsocketObfs return a HTTPObfs
|
||||||
|
func NewWebsocketObfs(conn net.Conn, option *WebsocketOption) (net.Conn, error) {
|
||||||
|
config := &vmess.WebsocketConfig{
|
||||||
|
Host: option.Host,
|
||||||
|
Path: option.Path,
|
||||||
|
TLS: option.TLSConfig != nil,
|
||||||
|
TLSConfig: option.TLSConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
conn, err = vmess.NewWebsocketConn(conn, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conn = NewMux(conn, MuxOption{
|
||||||
|
ID: [2]byte{0, 0},
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Port: 0,
|
||||||
|
})
|
||||||
|
return conn, nil
|
||||||
|
}
|
|
@ -69,7 +69,7 @@ type Client struct {
|
||||||
security Security
|
security Security
|
||||||
tls bool
|
tls bool
|
||||||
host string
|
host string
|
||||||
wsConfig *websocketConfig
|
wsConfig *WebsocketConfig
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ func (c *Client) New(conn net.Conn, dst *DstAddr) (net.Conn, error) {
|
||||||
var err error
|
var err error
|
||||||
r := rand.Intn(len(c.user))
|
r := rand.Intn(len(c.user))
|
||||||
if c.wsConfig != nil {
|
if c.wsConfig != nil {
|
||||||
conn, err = newWebsocketConn(conn, c.wsConfig)
|
conn, err = NewWebsocketConn(conn, c.wsConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -145,14 +145,14 @@ func NewClient(config Config) (*Client, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var wsConfig *websocketConfig
|
var wsConfig *WebsocketConfig
|
||||||
if config.NetWork == "ws" {
|
if config.NetWork == "ws" {
|
||||||
wsConfig = &websocketConfig{
|
wsConfig = &WebsocketConfig{
|
||||||
host: host,
|
Host: host,
|
||||||
path: config.WebSocketPath,
|
Path: config.WebSocketPath,
|
||||||
headers: config.WebSocketHeaders,
|
Headers: config.WebSocketHeaders,
|
||||||
tls: config.TLS,
|
TLS: config.TLS,
|
||||||
tlsConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,12 @@ type websocketConn struct {
|
||||||
remoteAddr net.Addr
|
remoteAddr net.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
type websocketConfig struct {
|
type WebsocketConfig struct {
|
||||||
host string
|
Host string
|
||||||
path string
|
Path string
|
||||||
headers map[string]string
|
Headers map[string]string
|
||||||
tls bool
|
TLS bool
|
||||||
tlsConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read implements net.Conn.Read()
|
// Read implements net.Conn.Read()
|
||||||
|
@ -102,7 +102,7 @@ func (wsc *websocketConn) SetWriteDeadline(t time.Time) error {
|
||||||
return wsc.conn.SetWriteDeadline(t)
|
return wsc.conn.SetWriteDeadline(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWebsocketConn(conn net.Conn, c *websocketConfig) (net.Conn, error) {
|
func NewWebsocketConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) {
|
||||||
dialer := &websocket.Dialer{
|
dialer := &websocket.Dialer{
|
||||||
NetDial: func(network, addr string) (net.Conn, error) {
|
NetDial: func(network, addr string) (net.Conn, error) {
|
||||||
return conn, nil
|
return conn, nil
|
||||||
|
@ -113,25 +113,25 @@ func newWebsocketConn(conn net.Conn, c *websocketConfig) (net.Conn, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
scheme := "ws"
|
scheme := "ws"
|
||||||
if c.tls {
|
if c.TLS {
|
||||||
scheme = "wss"
|
scheme = "wss"
|
||||||
dialer.TLSClientConfig = c.tlsConfig
|
dialer.TLSClientConfig = c.TLSConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
host, port, _ := net.SplitHostPort(c.host)
|
host, port, _ := net.SplitHostPort(c.Host)
|
||||||
if (scheme == "ws" && port != "80") || (scheme == "wss" && port != "443") {
|
if (scheme == "ws" && port != "80") || (scheme == "wss" && port != "443") {
|
||||||
host = c.host
|
host = c.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
uri := url.URL{
|
uri := url.URL{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
Host: host,
|
Host: host,
|
||||||
Path: c.path,
|
Path: c.Path,
|
||||||
}
|
}
|
||||||
|
|
||||||
headers := http.Header{}
|
headers := http.Header{}
|
||||||
if c.headers != nil {
|
if c.Headers != nil {
|
||||||
for k, v := range c.headers {
|
for k, v := range c.Headers {
|
||||||
headers.Set(k, v)
|
headers.Set(k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue