diff --git a/adapters/outbound/shadowsocks.go b/adapters/outbound/shadowsocks.go index feb8f098..abddbb37 100644 --- a/adapters/outbound/shadowsocks.go +++ b/adapters/outbound/shadowsocks.go @@ -2,12 +2,15 @@ package adapters import ( "bytes" + "crypto/tls" "encoding/json" "fmt" "net" "strconv" + "github.com/Dreamacro/clash/common/structure" obfs "github.com/Dreamacro/clash/component/simple-obfs" + v2rayObfs "github.com/Dreamacro/clash/component/v2ray-plugin" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/go-shadowsocks2/core" @@ -16,34 +19,60 @@ import ( type ShadowSocks struct { *Base - server string - obfs string - obfsHost string - cipher core.Cipher + server string + cipher core.Cipher + + // obfs + obfsMode string + obfsOption *simpleObfsOption + wsOption *v2rayObfs.WebsocketOption } type ShadowSocksOption struct { - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port"` - Password string `proxy:"password"` - Cipher string `proxy:"cipher"` + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + Password string `proxy:"password"` + 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"` 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) { c, err := net.DialTimeout("tcp", ss.server, tcpTimeout) 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) - switch ss.obfs { + switch ss.obfsMode { case "tls": - c = obfs.NewTLSObfs(c, ss.obfsHost) + c = obfs.NewTLSObfs(c, ss.obfsOption.Host) case "http": _, 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) _, 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()) } - obfs := option.Obfs - obfsHost := "bing.com" - if option.ObfsHost != "" { - obfsHost = option.ObfsHost + var wsOption *v2rayObfs.WebsocketOption + var obfsOption *simpleObfsOption + obfsMode := "" + + // 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{ @@ -76,10 +144,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { name: option.Name, tp: C.Shadowsocks, }, - server: server, - cipher: ciph, - obfs: obfs, - obfsHost: obfsHost, + server: server, + cipher: ciph, + + obfsMode: obfsMode, + wsOption: wsOption, + obfsOption: obfsOption, }, nil } diff --git a/common/structure/structure.go b/common/structure/structure.go index 600f264d..9d56b703 100644 --- a/common/structure/structure.go +++ b/common/structure/structure.go @@ -74,6 +74,8 @@ func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error return d.decodeSlice(name, data, val) case reflect.Map: return d.decodeMap(name, data, val) + case reflect.Interface: + return d.setInterface(name, data, val) default: 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 } + +func (d *Decoder) setInterface(name string, data interface{}, val reflect.Value) (err error) { + dataVal := reflect.ValueOf(data) + val.Set(dataVal) + return nil +} diff --git a/component/v2ray-plugin/mux.go b/component/v2ray-plugin/mux.go new file mode 100644 index 00000000..7191331b --- /dev/null +++ b/component/v2ray-plugin/mux.go @@ -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, + } +} diff --git a/component/v2ray-plugin/websocket.go b/component/v2ray-plugin/websocket.go new file mode 100644 index 00000000..03d6f854 --- /dev/null +++ b/component/v2ray-plugin/websocket.go @@ -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 +} diff --git a/component/vmess/vmess.go b/component/vmess/vmess.go index 604b45f6..e0a7a24f 100644 --- a/component/vmess/vmess.go +++ b/component/vmess/vmess.go @@ -69,7 +69,7 @@ type Client struct { security Security tls bool host string - wsConfig *websocketConfig + wsConfig *WebsocketConfig tlsConfig *tls.Config } @@ -93,7 +93,7 @@ func (c *Client) New(conn net.Conn, dst *DstAddr) (net.Conn, error) { var err error r := rand.Intn(len(c.user)) if c.wsConfig != nil { - conn, err = newWebsocketConn(conn, c.wsConfig) + conn, err = NewWebsocketConn(conn, c.wsConfig) if err != nil { return nil, err } @@ -145,14 +145,14 @@ func NewClient(config Config) (*Client, error) { } } - var wsConfig *websocketConfig + var wsConfig *WebsocketConfig if config.NetWork == "ws" { - wsConfig = &websocketConfig{ - host: host, - path: config.WebSocketPath, - headers: config.WebSocketHeaders, - tls: config.TLS, - tlsConfig: tlsConfig, + wsConfig = &WebsocketConfig{ + Host: host, + Path: config.WebSocketPath, + Headers: config.WebSocketHeaders, + TLS: config.TLS, + TLSConfig: tlsConfig, } } diff --git a/component/vmess/websocket.go b/component/vmess/websocket.go index 02e95d55..bd06c39d 100644 --- a/component/vmess/websocket.go +++ b/component/vmess/websocket.go @@ -19,12 +19,12 @@ type websocketConn struct { remoteAddr net.Addr } -type websocketConfig struct { - host string - path string - headers map[string]string - tls bool - tlsConfig *tls.Config +type WebsocketConfig struct { + Host string + Path string + Headers map[string]string + TLS bool + TLSConfig *tls.Config } // Read implements net.Conn.Read() @@ -102,7 +102,7 @@ func (wsc *websocketConn) SetWriteDeadline(t time.Time) error { 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{ NetDial: func(network, addr string) (net.Conn, error) { return conn, nil @@ -113,25 +113,25 @@ func newWebsocketConn(conn net.Conn, c *websocketConfig) (net.Conn, error) { } scheme := "ws" - if c.tls { + if c.TLS { 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") { - host = c.host + host = c.Host } uri := url.URL{ Scheme: scheme, Host: host, - Path: c.path, + Path: c.Path, } headers := http.Header{} - if c.headers != nil { - for k, v := range c.headers { + if c.Headers != nil { + for k, v := range c.Headers { headers.Set(k, v) } }