diff --git a/adapters/outbound/parser.go b/adapters/outbound/parser.go index 86ee68b5..19a779ee 100644 --- a/adapters/outbound/parser.go +++ b/adapters/outbound/parser.go @@ -24,6 +24,13 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) { break } proxy, err = NewShadowSocks(*ssOption) + case "ssr": + ssrOption := &ShadowSocksROption{} + err = decoder.Decode(mapping, ssrOption) + if err != nil { + break + } + proxy, err = NewShadowSocksR(*ssrOption) case "socks5": socksOption := &Socks5Option{} err = decoder.Decode(mapping, socksOption) diff --git a/adapters/outbound/shadowsocksr.go b/adapters/outbound/shadowsocksr.go new file mode 100644 index 00000000..1a422cdf --- /dev/null +++ b/adapters/outbound/shadowsocksr.go @@ -0,0 +1,134 @@ +package outbound + +import ( + "context" + "encoding/json" + "fmt" + "net" + "strconv" + + "github.com/Dreamacro/clash/component/dialer" + "github.com/Dreamacro/clash/component/ssr/obfs" + "github.com/Dreamacro/clash/component/ssr/protocol" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/go-shadowsocks2/core" + "github.com/Dreamacro/go-shadowsocks2/shadowstream" +) + +type ShadowSocksR struct { + *Base + cipher *core.StreamCipher + obfs obfs.Obfs + protocol protocol.Protocol +} + +type ShadowSocksROption struct { + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + Password string `proxy:"password"` + Cipher string `proxy:"cipher"` + Obfs string `proxy:"obfs"` + ObfsParam string `proxy:"obfs-param,omitempty"` + Protocol string `proxy:"protocol"` + ProtocolParam string `proxy:"protocol-param,omitempty"` + UDP bool `proxy:"udp,omitempty"` +} + +func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { + c = obfs.NewConn(c, ssr.obfs) + c = ssr.cipher.StreamConn(c) + conn, ok := c.(*shadowstream.Conn) + if !ok { + return nil, fmt.Errorf("invalid connection type") + } + iv, err := conn.ObtainWriteIV() + if err != nil { + return nil, err + } + c = protocol.NewConn(c, ssr.protocol, iv) + _, err = c.Write(serializesSocksAddr(metadata)) + return c, err +} + +func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { + c, err := dialer.DialContext(ctx, "tcp", ssr.addr) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err) + } + tcpKeepAlive(c) + + c, err = ssr.StreamConn(c, metadata) + return NewConn(c, ssr), err +} + +func (ssr *ShadowSocksR) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { + pc, err := dialer.ListenPacket("udp", "") + if err != nil { + return nil, err + } + + addr, err := resolveUDPAddr("udp", ssr.addr) + if err != nil { + return nil, err + } + + pc = ssr.cipher.PacketConn(pc) + pc = protocol.NewPacketConn(pc, ssr.protocol) + return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil +} + +func (ssr *ShadowSocksR) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]string{ + "type": ssr.Type().String(), + }) +} + +func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { + addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + cipher := option.Cipher + password := option.Password + coreCiph, err := core.PickCipher(cipher, nil, password) + if err != nil { + return nil, fmt.Errorf("ssr %s initialize cipher error: %w", addr, err) + } + ciph, ok := coreCiph.(*core.StreamCipher) + if !ok { + return nil, fmt.Errorf("%s is not a supported stream cipher in ssr", cipher) + } + + obfs, err := obfs.PickObfs(option.Obfs, &obfs.Base{ + IVSize: ciph.IVSize(), + Key: ciph.Key, + HeadLen: 30, + Host: option.Server, + Port: option.Port, + Param: option.ObfsParam, + }) + if err != nil { + return nil, fmt.Errorf("ssr %s initialize obfs error: %w", addr, err) + } + + protocol, err := protocol.PickProtocol(option.Protocol, &protocol.Base{ + IV: nil, + Key: ciph.Key, + TCPMss: 1460, + Param: option.ProtocolParam, + }) + if err != nil { + return nil, fmt.Errorf("ssr %s initialize protocol error: %w", addr, err) + } + protocol.SetOverhead(obfs.GetObfsOverhead() + protocol.GetProtocolOverhead()) + + return &ShadowSocksR{ + Base: &Base{ + name: option.Name, + addr: addr, + tp: C.ShadowsocksR, + udp: option.UDP, + }, + cipher: ciph, + obfs: obfs, + protocol: protocol, + }, nil +} diff --git a/component/ssr/obfs/base.go b/component/ssr/obfs/base.go new file mode 100644 index 00000000..0f58118e --- /dev/null +++ b/component/ssr/obfs/base.go @@ -0,0 +1,11 @@ +package obfs + +// Base information for obfs +type Base struct { + IVSize int + Key []byte + HeadLen int + Host string + Port int + Param string +} diff --git a/component/ssr/obfs/http_post.go b/component/ssr/obfs/http_post.go new file mode 100644 index 00000000..e50ece1c --- /dev/null +++ b/component/ssr/obfs/http_post.go @@ -0,0 +1,9 @@ +package obfs + +func init() { + register("http_post", newHTTPPost) +} + +func newHTTPPost(b *Base) Obfs { + return &httpObfs{Base: b, post: true} +} diff --git a/component/ssr/obfs/http_simple.go b/component/ssr/obfs/http_simple.go new file mode 100644 index 00000000..c485a661 --- /dev/null +++ b/component/ssr/obfs/http_simple.go @@ -0,0 +1,402 @@ +package obfs + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "math/rand" + "strings" +) + +type httpObfs struct { + *Base + firstRequest bool + firstResponse bool + post bool +} + +func init() { + register("http_simple", newHTTPSimple) +} + +func newHTTPSimple(b *Base) Obfs { + return &httpObfs{Base: b} +} + +func (h *httpObfs) initForConn() Obfs { + return &httpObfs{ + Base: h.Base, + firstRequest: true, + firstResponse: true, + post: h.post, + } +} + +func (h *httpObfs) GetObfsOverhead() int { + return 0 +} + +func (h *httpObfs) Decode(b []byte) ([]byte, bool, error) { + if h.firstResponse { + idx := bytes.Index(b, []byte("\r\n\r\n")) + if idx == -1 { + return nil, false, io.EOF + } + h.firstResponse = false + return b[idx+4:], false, nil + } + return b, false, nil +} + +func (h *httpObfs) Encode(b []byte) ([]byte, error) { + if h.firstRequest { + bSize := len(b) + var headData []byte + + if headSize := h.IVSize + h.HeadLen; bSize-headSize > 64 { + headData = make([]byte, headSize+rand.Intn(64)) + } else { + headData = make([]byte, bSize) + } + copy(headData, b[:len(headData)]) + host := h.Host + var customHead string + + if len(h.Param) > 0 { + customHeads := strings.Split(h.Param, "#") + if len(customHeads) > 2 { + customHeads = customHeads[:2] + } + customHosts := h.Param + if len(customHeads) > 1 { + customHosts = customHeads[0] + customHead = customHeads[1] + } + hosts := strings.Split(customHosts, ",") + if len(hosts) > 0 { + host = strings.TrimSpace(hosts[rand.Intn(len(hosts))]) + } + } + + method := "GET /" + if h.post { + method = "POST /" + } + requestPathIndex := rand.Intn(len(requestPath)/2) * 2 + httpBuf := fmt.Sprintf("%s%s%s%s HTTP/1.1\r\nHost: %s:%d\r\n", + method, + requestPath[requestPathIndex], + data2URLEncode(headData), + requestPath[requestPathIndex+1], + host, h.Port) + if len(customHead) > 0 { + httpBuf = httpBuf + strings.Replace(customHead, "\\n", "\r\n", -1) + "\r\n\r\n" + } else { + var contentType string + if h.post { + contentType = "Content-Type: multipart/form-data; boundary=" + boundary() + "\r\n" + } + httpBuf = httpBuf + "User-agent: " + requestUserAgent[rand.Intn(len(requestUserAgent))] + "\r\n" + + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + + "Accept-Language: en-US,en;q=0.8\r\n" + + "Accept-Encoding: gzip, deflate\r\n" + + contentType + + "DNT: 1\r\n" + + "Connection: keep-alive\r\n" + + "\r\n" + } + + var encoded []byte + if len(headData) < bSize { + encoded = make([]byte, len(httpBuf)+(bSize-len(headData))) + copy(encoded, []byte(httpBuf)) + copy(encoded[len(httpBuf):], b[len(headData):]) + } else { + encoded = []byte(httpBuf) + } + h.firstRequest = false + return encoded, nil + } + + return b, nil +} + +func data2URLEncode(data []byte) (ret string) { + for i := 0; i < len(data); i++ { + ret = fmt.Sprintf("%s%%%s", ret, hex.EncodeToString([]byte{data[i]})) + } + return +} + +func boundary() (ret string) { + set := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + for i := 0; i < 32; i++ { + ret = fmt.Sprintf("%s%c", ret, set[rand.Intn(len(set))]) + } + return +} + +var ( + requestPath = []string{ + "", "", + "login.php?redir=", "", + "register.php?code=", "", + "?keyword=", "", + "search?src=typd&q=", "&lang=en", + "s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&ch=&bar=&wd=", "&rn=", + "post.php?id=", "&goto=view.php", + } + requestUserAgent = []string{ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; Moto C Build/NRD90M.059) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1.1; SM-J120M Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; Moto G (5) Build/NPPS25.137-93-14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; SM-G570M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0; CAM-L03 Build/HUAWEICAM-L03) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3", + "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", + "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36", + "Mozilla/5.0 (X11; Datanyze; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1.1; SM-J111M Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; SM-J700M Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36", + "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Slackware/Chrome/12.0.742.100 Safari/534.30", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36", + "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Linux; Android 8.0.0; WAS-LX3 Build/HUAWEIWAS-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.1805 Safari/537.36 MVisionPlayer/1.0.0.0", + "Mozilla/5.0 (Linux; Android 7.0; TRT-LX3 Build/HUAWEITRT-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0; vivo 1610 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.2; de-de; SAMSUNG GT-I9195 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36", + "Mozilla/5.0 (Linux; Android 8.0.0; ANE-LX3 Build/HUAWEIANE-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "Mozilla/5.0 (X11; U; Linux i586; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; SM-G610M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; SM-J500M Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0; vivo 1606 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.1; vivo 1716 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0; MYA-L22 Build/HUAWEIMYA-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1; A1601 Build/LMY47I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; TRT-LX2 Build/HUAWEITRT-LX2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17", + "Mozilla/5.0 (Linux; Android 6.0; CAM-L21 Build/HUAWEICAM-L21; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24", + "Mozilla/5.0 (Linux; Android 7.1.2; Redmi 4X Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.2; SM-G7102 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1; HUAWEI CUN-L22 Build/HUAWEICUN-L22; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1.1; A37fw Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; SM-J730GM Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; SM-G610F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.1.2; Redmi Note 5A Build/N2G47H; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36", + "Mozilla/5.0 (Unknown; Linux) AppleWebKit/538.1 (KHTML, like Gecko) Chrome/v1.0.0 Safari/538.1", + "Mozilla/5.0 (Linux; Android 7.0; BLL-L22 Build/HUAWEIBLL-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; SM-J710F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.1.1; CPH1723 Build/N6F26Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36", + "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3 Build/HUAWEIFIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36 MVisionPlayer/1.0.0.0", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1; A37f Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; CPH1607 Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.116 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532G Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.83 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0; vivo 1713 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", + } +) diff --git a/component/ssr/obfs/obfs.go b/component/ssr/obfs/obfs.go new file mode 100644 index 00000000..1e8c4334 --- /dev/null +++ b/component/ssr/obfs/obfs.go @@ -0,0 +1,37 @@ +package obfs + +import ( + "errors" + "fmt" + "strings" +) + +var ( + errTLS12TicketAuthIncorrectMagicNumber = errors.New("tls1.2_ticket_auth incorrect magic number") + errTLS12TicketAuthTooShortData = errors.New("tls1.2_ticket_auth too short data") + errTLS12TicketAuthHMACError = errors.New("tls1.2_ticket_auth hmac verifying failed") +) + +// Obfs provides methods for decoding and encoding +type Obfs interface { + initForConn() Obfs + GetObfsOverhead() int + Decode(b []byte) ([]byte, bool, error) + Encode(b []byte) ([]byte, error) +} + +type obfsCreator func(b *Base) Obfs + +var obfsList = make(map[string]obfsCreator) + +func register(name string, c obfsCreator) { + obfsList[name] = c +} + +// PickObfs returns an obfs of the given name +func PickObfs(name string, b *Base) (Obfs, error) { + if obfsCreator, ok := obfsList[strings.ToLower(name)]; ok { + return obfsCreator(b), nil + } + return nil, fmt.Errorf("Obfs %s not supported", name) +} diff --git a/component/ssr/obfs/plain.go b/component/ssr/obfs/plain.go new file mode 100644 index 00000000..372feff2 --- /dev/null +++ b/component/ssr/obfs/plain.go @@ -0,0 +1,25 @@ +package obfs + +type plain struct{} + +func init() { + register("plain", newPlain) +} + +func newPlain(b *Base) Obfs { + return &plain{} +} + +func (p *plain) initForConn() Obfs { return &plain{} } + +func (p *plain) GetObfsOverhead() int { + return 0 +} + +func (p *plain) Encode(b []byte) ([]byte, error) { + return b, nil +} + +func (p *plain) Decode(b []byte) ([]byte, bool, error) { + return b, false, nil +} diff --git a/component/ssr/obfs/random_head.go b/component/ssr/obfs/random_head.go new file mode 100644 index 00000000..deaab2bd --- /dev/null +++ b/component/ssr/obfs/random_head.go @@ -0,0 +1,75 @@ +package obfs + +import ( + "encoding/binary" + "hash/crc32" + "math/rand" +) + +type randomHead struct { + *Base + firstRequest bool + firstResponse bool + headerSent bool + buffer []byte +} + +func init() { + register("random_head", newRandomHead) +} + +func newRandomHead(b *Base) Obfs { + return &randomHead{Base: b} +} + +func (r *randomHead) initForConn() Obfs { + return &randomHead{ + Base: r.Base, + firstRequest: true, + firstResponse: true, + } +} + +func (r *randomHead) GetObfsOverhead() int { + return 0 +} + +func (r *randomHead) Encode(b []byte) (encoded []byte, err error) { + if !r.firstRequest { + return b, nil + } + + bSize := len(b) + if r.headerSent { + if bSize > 0 { + d := make([]byte, len(r.buffer)+bSize) + copy(d, r.buffer) + copy(d[len(r.buffer):], b) + r.buffer = d + } else { + encoded = r.buffer + r.buffer = nil + r.firstRequest = false + } + } else { + size := rand.Intn(96) + 8 + encoded = make([]byte, size) + rand.Read(encoded) + crc := (0xFFFFFFFF - crc32.ChecksumIEEE(encoded[:size-4])) & 0xFFFFFFFF + binary.LittleEndian.PutUint32(encoded[size-4:], crc) + + d := make([]byte, bSize) + copy(d, b) + r.buffer = d + } + r.headerSent = true + return encoded, nil +} + +func (r *randomHead) Decode(b []byte) ([]byte, bool, error) { + if r.firstResponse { + r.firstResponse = false + return b, true, nil + } + return b, false, nil +} diff --git a/component/ssr/obfs/stream.go b/component/ssr/obfs/stream.go new file mode 100644 index 00000000..f94cf85c --- /dev/null +++ b/component/ssr/obfs/stream.go @@ -0,0 +1,72 @@ +package obfs + +import ( + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +// NewConn wraps a stream-oriented net.Conn with obfs decoding/encoding +func NewConn(c net.Conn, o Obfs) net.Conn { + return &Conn{Conn: c, Obfs: o.initForConn()} +} + +// Conn represents an obfs connection +type Conn struct { + net.Conn + Obfs + buf []byte + offset int +} + +func (c *Conn) Read(b []byte) (int, error) { + if c.buf != nil { + n := copy(b, c.buf[c.offset:]) + c.offset += n + if c.offset == len(c.buf) { + pool.Put(c.buf) + c.buf = nil + } + return n, nil + } + + buf := pool.Get(pool.RelayBufferSize) + defer pool.Put(buf) + n, err := c.Conn.Read(buf) + if err != nil { + return 0, err + } + decoded, sendback, err := c.Decode(buf[:n]) + // decoded may be part of buf + decodedData := pool.Get(len(decoded)) + copy(decodedData, decoded) + if err != nil { + pool.Put(decodedData) + return 0, err + } + if sendback { + c.Write(nil) + pool.Put(decodedData) + return 0, nil + } + n = copy(b, decodedData) + if len(decodedData) > len(b) { + c.buf = decodedData + c.offset = n + } else { + pool.Put(decodedData) + } + return n, err +} + +func (c *Conn) Write(b []byte) (int, error) { + encoded, err := c.Encode(b) + if err != nil { + return 0, err + } + _, err = c.Conn.Write(encoded) + if err != nil { + return 0, err + } + return len(b), nil +} diff --git a/component/ssr/obfs/tls12_ticket_auth.go b/component/ssr/obfs/tls12_ticket_auth.go new file mode 100644 index 00000000..9cb31d44 --- /dev/null +++ b/component/ssr/obfs/tls12_ticket_auth.go @@ -0,0 +1,291 @@ +package obfs + +import ( + "bytes" + "crypto/hmac" + "encoding/binary" + "fmt" + "io" + "log" + "math/rand" + "strings" + "time" + + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/component/ssr/tools" +) + +type tlsAuthData struct { + localClientID [32]byte +} + +type tls12Ticket struct { + *Base + *tlsAuthData + handshakeStatus int + sendSaver bytes.Buffer + recvBuffer bytes.Buffer + buffer bytes.Buffer +} + +func init() { + register("tls1.2_ticket_auth", newTLS12Ticket) + register("tls1.2_ticket_fastauth", newTLS12Ticket) +} + +func newTLS12Ticket(b *Base) Obfs { + return &tls12Ticket{ + Base: b, + } +} + +func (t *tls12Ticket) initForConn() Obfs { + r := &tls12Ticket{ + Base: t.Base, + tlsAuthData: &tlsAuthData{}, + } + rand.Read(r.localClientID[:]) + return r +} + +func (t *tls12Ticket) GetObfsOverhead() int { + return 5 +} + +func (t *tls12Ticket) Decode(b []byte) ([]byte, bool, error) { + if t.handshakeStatus == -1 { + return b, false, nil + } + t.buffer.Reset() + if t.handshakeStatus == 8 { + t.recvBuffer.Write(b) + for t.recvBuffer.Len() > 5 { + var h [5]byte + t.recvBuffer.Read(h[:]) + if !bytes.Equal(h[:3], []byte{0x17, 0x3, 0x3}) { + log.Println("incorrect magic number", h[:3], ", 0x170303 is expected") + return nil, false, errTLS12TicketAuthIncorrectMagicNumber + } + size := int(binary.BigEndian.Uint16(h[3:5])) + if t.recvBuffer.Len() < size { + // 不够读,下回再读吧 + unread := t.recvBuffer.Bytes() + t.recvBuffer.Reset() + t.recvBuffer.Write(h[:]) + t.recvBuffer.Write(unread) + break + } + d := pool.Get(size) + t.recvBuffer.Read(d) + t.buffer.Write(d) + pool.Put(d) + } + return t.buffer.Bytes(), false, nil + } + + if len(b) < 11+32+1+32 { + return nil, false, errTLS12TicketAuthTooShortData + } + + hash := t.hmacSHA1(b[11 : 11+22]) + + if !hmac.Equal(b[33:33+tools.HmacSHA1Len], hash) { + return nil, false, errTLS12TicketAuthHMACError + } + return nil, true, nil +} + +func (t *tls12Ticket) Encode(b []byte) ([]byte, error) { + t.buffer.Reset() + switch t.handshakeStatus { + case 8: + if len(b) < 1024 { + d := []byte{0x17, 0x3, 0x3, 0, 0} + binary.BigEndian.PutUint16(d[3:5], uint16(len(b)&0xFFFF)) + t.buffer.Write(d) + t.buffer.Write(b) + return t.buffer.Bytes(), nil + } + start := 0 + var l int + for len(b)-start > 2048 { + l = rand.Intn(4096) + 100 + if l > len(b)-start { + l = len(b) - start + } + packData(&t.buffer, b[start:start+l]) + start += l + } + if len(b)-start > 0 { + l = len(b) - start + packData(&t.buffer, b[start:start+l]) + } + return t.buffer.Bytes(), nil + case 1: + if len(b) > 0 { + if len(b) < 1024 { + packData(&t.sendSaver, b) + } else { + start := 0 + var l int + for len(b)-start > 2048 { + l = rand.Intn(4096) + 100 + if l > len(b)-start { + l = len(b) - start + } + packData(&t.buffer, b[start:start+l]) + start += l + } + if len(b)-start > 0 { + l = len(b) - start + packData(&t.buffer, b[start:start+l]) + } + io.Copy(&t.sendSaver, &t.buffer) + } + return []byte{}, nil + } + hmacData := make([]byte, 43) + handshakeFinish := []byte("\x14\x03\x03\x00\x01\x01\x16\x03\x03\x00\x20") + copy(hmacData, handshakeFinish) + rand.Read(hmacData[11:33]) + h := t.hmacSHA1(hmacData[:33]) + copy(hmacData[33:], h) + t.buffer.Write(hmacData) + io.Copy(&t.buffer, &t.sendSaver) + t.handshakeStatus = 8 + return t.buffer.Bytes(), nil + case 0: + tlsData0 := []byte("\x00\x1c\xc0\x2b\xc0\x2f\xcc\xa9\xcc\xa8\xcc\x14\xcc\x13\xc0\x0a\xc0\x14\xc0\x09\xc0\x13\x00\x9c\x00\x35\x00\x2f\x00\x0a\x01\x00") + tlsData1 := []byte("\xff\x01\x00\x01\x00") + tlsData2 := []byte("\x00\x17\x00\x00\x00\x23\x00\xd0") + // tlsData3 := []byte("\x00\x0d\x00\x16\x00\x14\x06\x01\x06\x03\x05\x01\x05\x03\x04\x01\x04\x03\x03\x01\x03\x03\x02\x01\x02\x03\x00\x05\x00\x05\x01\x00\x00\x00\x00\x00\x12\x00\x00\x75\x50\x00\x00\x00\x0b\x00\x02\x01\x00\x00\x0a\x00\x06\x00\x04\x00\x17\x00\x18\x00\x15\x00\x66\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") + tlsData3 := []byte("\x00\x0d\x00\x16\x00\x14\x06\x01\x06\x03\x05\x01\x05\x03\x04\x01\x04\x03\x03\x01\x03\x03\x02\x01\x02\x03\x00\x05\x00\x05\x01\x00\x00\x00\x00\x00\x12\x00\x00\x75\x50\x00\x00\x00\x0b\x00\x02\x01\x00\x00\x0a\x00\x06\x00\x04\x00\x17\x00\x18") + + var tlsData [2048]byte + tlsDataLen := 0 + copy(tlsData[0:], tlsData1) + tlsDataLen += len(tlsData1) + sni := t.sni(t.getHost()) + copy(tlsData[tlsDataLen:], sni) + tlsDataLen += len(sni) + copy(tlsData[tlsDataLen:], tlsData2) + tlsDataLen += len(tlsData2) + ticketLen := rand.Intn(164)*2 + 64 + tlsData[tlsDataLen-1] = uint8(ticketLen & 0xff) + tlsData[tlsDataLen-2] = uint8(ticketLen >> 8) + //ticketLen := 208 + rand.Read(tlsData[tlsDataLen : tlsDataLen+ticketLen]) + tlsDataLen += ticketLen + copy(tlsData[tlsDataLen:], tlsData3) + tlsDataLen += len(tlsData3) + + length := 11 + 32 + 1 + 32 + len(tlsData0) + 2 + tlsDataLen + encodedData := make([]byte, length) + pdata := length - tlsDataLen + l := tlsDataLen + copy(encodedData[pdata:], tlsData[:tlsDataLen]) + encodedData[pdata-1] = uint8(tlsDataLen) + encodedData[pdata-2] = uint8(tlsDataLen >> 8) + pdata -= 2 + l += 2 + copy(encodedData[pdata-len(tlsData0):], tlsData0) + pdata -= len(tlsData0) + l += len(tlsData0) + copy(encodedData[pdata-32:], t.localClientID[:]) + pdata -= 32 + l += 32 + encodedData[pdata-1] = 0x20 + pdata-- + l++ + copy(encodedData[pdata-32:], t.packAuthData()) + pdata -= 32 + l += 32 + encodedData[pdata-1] = 0x3 + encodedData[pdata-2] = 0x3 // tls version + pdata -= 2 + l += 2 + encodedData[pdata-1] = uint8(l) + encodedData[pdata-2] = uint8(l >> 8) + encodedData[pdata-3] = 0 + encodedData[pdata-4] = 1 + pdata -= 4 + l += 4 + encodedData[pdata-1] = uint8(l) + encodedData[pdata-2] = uint8(l >> 8) + pdata -= 2 + l += 2 + encodedData[pdata-1] = 0x1 + encodedData[pdata-2] = 0x3 // tls version + pdata -= 2 + l += 2 + encodedData[pdata-1] = 0x16 // tls handshake + pdata-- + l++ + packData(&t.sendSaver, b) + t.handshakeStatus = 1 + return encodedData, nil + default: + return nil, fmt.Errorf("unexpected handshake status: %d", t.handshakeStatus) + } +} + +func (t *tls12Ticket) hmacSHA1(data []byte) []byte { + key := make([]byte, len(t.Key)+32) + copy(key, t.Key) + copy(key[len(t.Key):], t.localClientID[:]) + + sha1Data := tools.HmacSHA1(key, data) + return sha1Data[:tools.HmacSHA1Len] +} + +func (t *tls12Ticket) sni(u string) []byte { + bURL := []byte(u) + length := len(bURL) + ret := make([]byte, length+9) + copy(ret[9:9+length], bURL) + binary.BigEndian.PutUint16(ret[7:], uint16(length&0xFFFF)) + length += 3 + binary.BigEndian.PutUint16(ret[4:], uint16(length&0xFFFF)) + length += 2 + binary.BigEndian.PutUint16(ret[2:], uint16(length&0xFFFF)) + return ret +} + +func (t *tls12Ticket) getHost() string { + host := t.Host + if len(t.Param) > 0 { + hosts := strings.Split(t.Param, ",") + if len(hosts) > 0 { + + host = hosts[rand.Intn(len(hosts))] + host = strings.TrimSpace(host) + } + } + if len(host) > 0 && host[len(host)-1] >= byte('0') && host[len(host)-1] <= byte('9') && len(t.Param) == 0 { + host = "" + } + return host +} + +func (t *tls12Ticket) packAuthData() (ret []byte) { + retSize := 32 + ret = make([]byte, retSize) + + now := time.Now().Unix() + binary.BigEndian.PutUint32(ret[:4], uint32(now)) + + rand.Read(ret[4 : 4+18]) + + hash := t.hmacSHA1(ret[:retSize-tools.HmacSHA1Len]) + copy(ret[retSize-tools.HmacSHA1Len:], hash) + + return +} + +func packData(buffer *bytes.Buffer, suffix []byte) { + d := []byte{0x17, 0x3, 0x3, 0, 0} + binary.BigEndian.PutUint16(d[3:5], uint16(len(suffix)&0xFFFF)) + buffer.Write(d) + buffer.Write(suffix) + return +} diff --git a/component/ssr/protocol/auth_aes128_md5.go b/component/ssr/protocol/auth_aes128_md5.go new file mode 100644 index 00000000..cbe56921 --- /dev/null +++ b/component/ssr/protocol/auth_aes128_md5.go @@ -0,0 +1,300 @@ +package protocol + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "encoding/binary" + "math/rand" + "strconv" + "strings" + "time" + + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/component/ssr/tools" + "github.com/Dreamacro/go-shadowsocks2/core" +) + +type authAES128 struct { + *Base + *recvInfo + *authData + hasSentHeader bool + packID uint32 + userKey []byte + uid [4]byte + salt string + hmac hmacMethod + hashDigest hashDigestMethod +} + +func init() { + register("auth_aes128_md5", newAuthAES128MD5) +} + +func newAuthAES128MD5(b *Base) Protocol { + return &authAES128{ + Base: b, + authData: &authData{}, + salt: "auth_aes128_md5", + hmac: tools.HmacMD5, + hashDigest: tools.MD5Sum, + } +} + +func (a *authAES128) initForConn(iv []byte) Protocol { + return &authAES128{ + Base: &Base{ + IV: iv, + Key: a.Key, + TCPMss: a.TCPMss, + Overhead: a.Overhead, + Param: a.Param, + }, + recvInfo: &recvInfo{recvID: 1, buffer: new(bytes.Buffer)}, + authData: a.authData, + packID: 1, + salt: a.salt, + hmac: a.hmac, + hashDigest: a.hashDigest, + } +} + +func (a *authAES128) GetProtocolOverhead() int { + return 9 +} + +func (a *authAES128) SetOverhead(overhead int) { + a.Overhead = overhead +} + +func (a *authAES128) Decode(b []byte) ([]byte, int, error) { + a.buffer.Reset() + bSize := len(b) + readSize := 0 + key := pool.Get(len(a.userKey) + 4) + defer pool.Put(key) + copy(key, a.userKey) + for bSize > 4 { + binary.LittleEndian.PutUint32(key[len(key)-4:], a.recvID) + + h := a.hmac(key, b[:2]) + if !bytes.Equal(h[:2], b[2:4]) { + return nil, 0, errAuthAES128HMACError + } + length := int(binary.LittleEndian.Uint16(b[:2])) + if length >= 8192 || length < 8 { + return nil, 0, errAuthAES128DataLengthError + } + if length > bSize { + break + } + a.recvID++ + pos := int(b[4]) + if pos < 255 { + pos += 4 + } else { + pos = int(binary.LittleEndian.Uint16(b[5:7])) + 4 + } + + a.buffer.Write(b[pos : length-4]) + b = b[length:] + bSize -= length + readSize += length + } + return a.buffer.Bytes(), readSize, nil +} + +func (a *authAES128) Encode(b []byte) ([]byte, error) { + a.buffer.Reset() + bSize := len(b) + offset := 0 + if bSize > 0 && !a.hasSentHeader { + authSize := bSize + if authSize > 1200 { + authSize = 1200 + } + a.hasSentHeader = true + a.buffer.Write(a.packAuthData(b[:authSize])) + bSize -= authSize + offset += authSize + } + const blockSize = 4096 + for bSize > blockSize { + packSize, randSize := a.packedDataSize(b[offset : offset+blockSize]) + pack := pool.Get(packSize) + a.packData(b[offset:offset+blockSize], pack, randSize) + a.buffer.Write(pack) + pool.Put(pack) + bSize -= blockSize + offset += blockSize + } + if bSize > 0 { + packSize, randSize := a.packedDataSize(b[offset:]) + pack := pool.Get(packSize) + a.packData(b[offset:], pack, randSize) + a.buffer.Write(pack) + pool.Put(pack) + } + return a.buffer.Bytes(), nil +} + +func (a *authAES128) DecodePacket(b []byte) ([]byte, int, error) { + bSize := len(b) + h := a.hmac(a.Key, b[:bSize-4]) + if !bytes.Equal(h[:4], b[bSize-4:]) { + return nil, 0, errAuthAES128HMACError + } + return b[:bSize-4], bSize - 4, nil +} + +func (a *authAES128) EncodePacket(b []byte) ([]byte, error) { + a.initUserKeyAndID() + + var buf bytes.Buffer + buf.Write(b) + buf.Write(a.uid[:]) + h := a.hmac(a.userKey, buf.Bytes()) + buf.Write(h[:4]) + return buf.Bytes(), nil +} + +func (a *authAES128) initUserKeyAndID() { + if a.userKey == nil { + params := strings.Split(a.Param, ":") + if len(params) >= 2 { + if userID, err := strconv.ParseUint(params[0], 10, 32); err == nil { + binary.LittleEndian.PutUint32(a.uid[:], uint32(userID)) + a.userKey = a.hashDigest([]byte(params[1])) + } + } + + if a.userKey == nil { + rand.Read(a.uid[:]) + a.userKey = make([]byte, len(a.Key)) + copy(a.userKey, a.Key) + } + } +} + +func (a *authAES128) packedDataSize(data []byte) (packSize, randSize int) { + dataSize := len(data) + randSize = 1 + if dataSize <= 1200 { + if a.packID > 4 { + randSize += rand.Intn(32) + } else { + if dataSize > 900 { + randSize += rand.Intn(128) + } else { + randSize += rand.Intn(512) + } + } + } + packSize = randSize + dataSize + 8 + return +} + +func (a *authAES128) packData(data, ret []byte, randSize int) { + dataSize := len(data) + retSize := len(ret) + // 0~1, ret_size + binary.LittleEndian.PutUint16(ret[0:], uint16(retSize&0xFFFF)) + // 2~3, hmac + key := pool.Get(len(a.userKey) + 4) + defer pool.Put(key) + copy(key, a.userKey) + binary.LittleEndian.PutUint32(key[len(key)-4:], a.packID) + h := a.hmac(key, ret[:2]) + copy(ret[2:4], h[:2]) + // 4~rand_size+4, rand number + rand.Read(ret[4 : 4+randSize]) + // 4, rand_size + if randSize < 128 { + ret[4] = byte(randSize & 0xFF) + } else { + // 4, magic number 0xFF + ret[4] = 0xFF + // 5~6, rand_size + binary.LittleEndian.PutUint16(ret[5:], uint16(randSize&0xFFFF)) + } + // rand_size+4~ret_size-4, data + if dataSize > 0 { + copy(ret[randSize+4:], data) + } + a.packID++ + h = a.hmac(key, ret[:retSize-4]) + copy(ret[retSize-4:], h[:4]) +} + +func (a *authAES128) packAuthData(data []byte) (ret []byte) { + dataSize := len(data) + var randSize int + + if dataSize > 400 { + randSize = rand.Intn(512) + } else { + randSize = rand.Intn(1024) + } + + dataOffset := randSize + 16 + 4 + 4 + 7 + retSize := dataOffset + dataSize + 4 + ret = make([]byte, retSize) + encrypt := make([]byte, 24) + key := make([]byte, len(a.IV)+len(a.Key)) + copy(key, a.IV) + copy(key[len(a.IV):], a.Key) + + rand.Read(ret[dataOffset-randSize:]) + a.mutex.Lock() + defer a.mutex.Unlock() + a.connectionID++ + if a.connectionID > 0xFF000000 { + a.clientID = nil + } + if len(a.clientID) == 0 { + a.clientID = make([]byte, 8) + rand.Read(a.clientID) + b := make([]byte, 4) + rand.Read(b) + a.connectionID = binary.LittleEndian.Uint32(b) & 0xFFFFFF + } + copy(encrypt[4:], a.clientID) + binary.LittleEndian.PutUint32(encrypt[8:], a.connectionID) + + now := time.Now().Unix() + binary.LittleEndian.PutUint32(encrypt[:4], uint32(now)) + + binary.LittleEndian.PutUint16(encrypt[12:], uint16(retSize&0xFFFF)) + binary.LittleEndian.PutUint16(encrypt[14:], uint16(randSize&0xFFFF)) + + a.initUserKeyAndID() + + aesCipherKey := core.Kdf(base64.StdEncoding.EncodeToString(a.userKey)+a.salt, 16) + block, err := aes.NewCipher(aesCipherKey) + if err != nil { + return nil + } + encryptData := make([]byte, 16) + iv := make([]byte, aes.BlockSize) + cbc := cipher.NewCBCEncrypter(block, iv) + cbc.CryptBlocks(encryptData, encrypt[:16]) + copy(encrypt[:4], a.uid[:]) + copy(encrypt[4:4+16], encryptData) + + h := a.hmac(key, encrypt[:20]) + copy(encrypt[20:], h[:4]) + + rand.Read(ret[:1]) + h = a.hmac(key, ret[:1]) + copy(ret[1:], h[:7-1]) + + copy(ret[7:], encrypt) + copy(ret[dataOffset:], data) + + h = a.hmac(a.userKey, ret[:retSize-4]) + copy(ret[retSize-4:], h[:4]) + + return +} diff --git a/component/ssr/protocol/auth_aes128_sha1.go b/component/ssr/protocol/auth_aes128_sha1.go new file mode 100644 index 00000000..d549b588 --- /dev/null +++ b/component/ssr/protocol/auth_aes128_sha1.go @@ -0,0 +1,22 @@ +package protocol + +import ( + "bytes" + + "github.com/Dreamacro/clash/component/ssr/tools" +) + +func init() { + register("auth_aes128_sha1", newAuthAES128SHA1) +} + +func newAuthAES128SHA1(b *Base) Protocol { + return &authAES128{ + Base: b, + recvInfo: &recvInfo{buffer: new(bytes.Buffer)}, + authData: &authData{}, + salt: "auth_aes128_sha1", + hmac: tools.HmacSHA1, + hashDigest: tools.SHA1Sum, + } +} diff --git a/component/ssr/protocol/auth_chain_a.go b/component/ssr/protocol/auth_chain_a.go new file mode 100644 index 00000000..a00bad68 --- /dev/null +++ b/component/ssr/protocol/auth_chain_a.go @@ -0,0 +1,428 @@ +package protocol + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rc4" + "encoding/base64" + "encoding/binary" + "math/rand" + "strconv" + "strings" + "time" + + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/component/ssr/tools" + "github.com/Dreamacro/go-shadowsocks2/core" +) + +type authChain struct { + *Base + *recvInfo + *authData + randomClient shift128PlusContext + randomServer shift128PlusContext + enc cipher.Stream + dec cipher.Stream + headerSent bool + lastClientHash []byte + lastServerHash []byte + userKey []byte + uid [4]byte + salt string + hmac hmacMethod + hashDigest hashDigestMethod + rnd rndMethod + dataSizeList []int + dataSizeList2 []int + chunkID uint32 +} + +func init() { + register("auth_chain_a", newAuthChainA) +} + +func newAuthChainA(b *Base) Protocol { + return &authChain{ + Base: b, + authData: &authData{}, + salt: "auth_chain_a", + hmac: tools.HmacMD5, + hashDigest: tools.SHA1Sum, + rnd: authChainAGetRandLen, + } +} + +func (a *authChain) initForConn(iv []byte) Protocol { + r := &authChain{ + Base: &Base{ + IV: iv, + Key: a.Key, + TCPMss: a.TCPMss, + Overhead: a.Overhead, + Param: a.Param, + }, + recvInfo: &recvInfo{recvID: 1, buffer: new(bytes.Buffer)}, + authData: a.authData, + salt: a.salt, + hmac: a.hmac, + hashDigest: a.hashDigest, + rnd: a.rnd, + } + if r.salt == "auth_chain_b" { + initDataSize(r) + } + return r +} + +func (a *authChain) GetProtocolOverhead() int { + return 4 +} + +func (a *authChain) SetOverhead(overhead int) { + a.Overhead = overhead +} + +func (a *authChain) Decode(b []byte) ([]byte, int, error) { + a.buffer.Reset() + key := pool.Get(len(a.userKey) + 4) + defer pool.Put(key) + readSize := 0 + copy(key, a.userKey) + for len(b) > 4 { + binary.LittleEndian.PutUint32(key[len(a.userKey):], a.recvID) + dataLen := (int)((uint(b[1]^a.lastServerHash[15]) << 8) + uint(b[0]^a.lastServerHash[14])) + randLen := a.getServerRandLen(dataLen, a.Overhead) + length := randLen + dataLen + if length >= 4096 { + return nil, 0, errAuthChainDataLengthError + } + length += 4 + if length > len(b) { + break + } + + hash := a.hmac(key, b[:length-2]) + if !bytes.Equal(hash[:2], b[length-2:length]) { + return nil, 0, errAuthChainHMACError + } + var dataPos int + if dataLen > 0 && randLen > 0 { + dataPos = 2 + getRandStartPos(&a.randomServer, randLen) + } else { + dataPos = 2 + } + d := pool.Get(dataLen) + a.dec.XORKeyStream(d, b[dataPos:dataPos+dataLen]) + a.buffer.Write(d) + pool.Put(d) + if a.recvID == 1 { + a.TCPMss = int(binary.LittleEndian.Uint16(a.buffer.Next(2))) + } + a.lastServerHash = hash + a.recvID++ + b = b[length:] + readSize += length + } + return a.buffer.Bytes(), readSize, nil +} + +func (a *authChain) Encode(b []byte) ([]byte, error) { + a.buffer.Reset() + bSize := len(b) + offset := 0 + if bSize > 0 && !a.headerSent { + headSize := 1200 + if headSize > bSize { + headSize = bSize + } + a.buffer.Write(a.packAuthData(b[:headSize])) + offset += headSize + bSize -= headSize + a.headerSent = true + } + var unitSize = a.TCPMss - a.Overhead + for bSize > unitSize { + dataLen, randLength := a.packedDataLen(b[offset : offset+unitSize]) + d := pool.Get(dataLen) + a.packData(d, b[offset:offset+unitSize], randLength) + a.buffer.Write(d) + pool.Put(d) + bSize -= unitSize + offset += unitSize + } + if bSize > 0 { + dataLen, randLength := a.packedDataLen(b[offset:]) + d := pool.Get(dataLen) + a.packData(d, b[offset:], randLength) + a.buffer.Write(d) + pool.Put(d) + } + return a.buffer.Bytes(), nil +} + +func (a *authChain) DecodePacket(b []byte) ([]byte, int, error) { + bSize := len(b) + if bSize < 9 { + return nil, 0, errAuthChainDataLengthError + } + h := a.hmac(a.userKey, b[:bSize-1]) + if h[0] != b[bSize-1] { + return nil, 0, errAuthChainHMACError + } + hash := a.hmac(a.Key, b[bSize-8:bSize-1]) + cipherKey := a.getRC4CipherKey(hash) + dec, _ := rc4.NewCipher(cipherKey) + randLength := udpGetRandLen(&a.randomServer, hash) + bSize -= 8 + randLength + dec.XORKeyStream(b, b[:bSize]) + return b, bSize, nil +} + +func (a *authChain) EncodePacket(b []byte) ([]byte, error) { + a.initUserKeyAndID() + authData := pool.Get(3) + defer pool.Put(authData) + rand.Read(authData) + hash := a.hmac(a.Key, authData) + uid := pool.Get(4) + defer pool.Put(uid) + for i := 0; i < 4; i++ { + uid[i] = a.uid[i] ^ hash[i] + } + + cipherKey := a.getRC4CipherKey(hash) + enc, _ := rc4.NewCipher(cipherKey) + var buf bytes.Buffer + enc.XORKeyStream(b, b) + buf.Write(b) + + randLength := udpGetRandLen(&a.randomClient, hash) + randBytes := pool.Get(randLength) + defer pool.Put(randBytes) + buf.Write(randBytes) + + buf.Write(authData) + buf.Write(uid) + + h := a.hmac(a.userKey, buf.Bytes()) + buf.Write(h[:1]) + return buf.Bytes(), nil +} + +func (a *authChain) getRC4CipherKey(hash []byte) []byte { + base64UserKey := base64.StdEncoding.EncodeToString(a.userKey) + return a.calcRC4CipherKey(hash, base64UserKey) +} + +func (a *authChain) calcRC4CipherKey(hash []byte, base64UserKey string) []byte { + password := pool.Get(len(base64UserKey) + base64.StdEncoding.EncodedLen(16)) + defer pool.Put(password) + copy(password, base64UserKey) + base64.StdEncoding.Encode(password[len(base64UserKey):], hash[:16]) + return core.Kdf(string(password), 16) +} + +func (a *authChain) initUserKeyAndID() { + if a.userKey == nil { + params := strings.Split(a.Param, ":") + if len(params) >= 2 { + if userID, err := strconv.ParseUint(params[0], 10, 32); err == nil { + binary.LittleEndian.PutUint32(a.uid[:], uint32(userID)) + a.userKey = []byte(params[1])[:len(a.userKey)] + } + } + + if a.userKey == nil { + rand.Read(a.uid[:]) + a.userKey = make([]byte, len(a.Key)) + copy(a.userKey, a.Key) + } + } +} + +func (a *authChain) getClientRandLen(dataLength int, overhead int) int { + return a.rnd(dataLength, &a.randomClient, a.lastClientHash, a.dataSizeList, a.dataSizeList2, overhead) +} + +func (a *authChain) getServerRandLen(dataLength int, overhead int) int { + return a.rnd(dataLength, &a.randomServer, a.lastServerHash, a.dataSizeList, a.dataSizeList2, overhead) +} + +func (a *authChain) packedDataLen(data []byte) (chunkLength, randLength int) { + dataLength := len(data) + randLength = a.getClientRandLen(dataLength, a.Overhead) + chunkLength = randLength + dataLength + 2 + 2 + return +} + +func (a *authChain) packData(outData []byte, data []byte, randLength int) { + dataLength := len(data) + outLength := randLength + dataLength + 2 + outData[0] = byte(dataLength) ^ a.lastClientHash[14] + outData[1] = byte(dataLength>>8) ^ a.lastClientHash[15] + + { + if dataLength > 0 { + randPart1Length := getRandStartPos(&a.randomClient, randLength) + rand.Read(outData[2 : 2+randPart1Length]) + a.enc.XORKeyStream(outData[2+randPart1Length:], data) + rand.Read(outData[2+randPart1Length+dataLength : outLength]) + } else { + rand.Read(outData[2 : 2+randLength]) + } + } + + userKeyLen := uint8(len(a.userKey)) + key := pool.Get(int(userKeyLen + 4)) + defer pool.Put(key) + copy(key, a.userKey) + a.chunkID++ + binary.LittleEndian.PutUint32(key[userKeyLen:], a.chunkID) + a.lastClientHash = a.hmac(key, outData[:outLength]) + copy(outData[outLength:], a.lastClientHash[:2]) + return +} + +const authHeadLength = 4 + 8 + 4 + 16 + 4 + +func (a *authChain) packAuthData(data []byte) (outData []byte) { + outData = make([]byte, authHeadLength, authHeadLength+1500) + a.mutex.Lock() + defer a.mutex.Unlock() + a.connectionID++ + if a.connectionID > 0xFF000000 { + rand.Read(a.clientID) + b := make([]byte, 4) + rand.Read(b) + a.connectionID = binary.LittleEndian.Uint32(b) & 0xFFFFFF + } + var key = make([]byte, len(a.IV)+len(a.Key)) + copy(key, a.IV) + copy(key[len(a.IV):], a.Key) + + encrypt := make([]byte, 20) + t := time.Now().Unix() + binary.LittleEndian.PutUint32(encrypt[:4], uint32(t)) + copy(encrypt[4:8], a.clientID) + binary.LittleEndian.PutUint32(encrypt[8:], a.connectionID) + binary.LittleEndian.PutUint16(encrypt[12:], uint16(a.Overhead)) + binary.LittleEndian.PutUint16(encrypt[14:], 0) + + // first 12 bytes + { + rand.Read(outData[:4]) + a.lastClientHash = a.hmac(key, outData[:4]) + copy(outData[4:], a.lastClientHash[:8]) + } + var base64UserKey string + // uid & 16 bytes auth data + { + a.initUserKeyAndID() + uid := make([]byte, 4) + for i := 0; i < 4; i++ { + uid[i] = a.uid[i] ^ a.lastClientHash[8+i] + } + base64UserKey = base64.StdEncoding.EncodeToString(a.userKey) + aesCipherKey := core.Kdf(base64UserKey+a.salt, 16) + block, err := aes.NewCipher(aesCipherKey) + if err != nil { + return + } + encryptData := make([]byte, 16) + iv := make([]byte, aes.BlockSize) + cbc := cipher.NewCBCEncrypter(block, iv) + cbc.CryptBlocks(encryptData, encrypt[:16]) + copy(encrypt[:4], uid[:]) + copy(encrypt[4:4+16], encryptData) + } + // final HMAC + { + a.lastServerHash = a.hmac(a.userKey, encrypt[:20]) + + copy(outData[12:], encrypt) + copy(outData[12+20:], a.lastServerHash[:4]) + } + + // init cipher + cipherKey := a.calcRC4CipherKey(a.lastClientHash, base64UserKey) + a.enc, _ = rc4.NewCipher(cipherKey) + a.dec, _ = rc4.NewCipher(cipherKey) + + // data + chunkLength, randLength := a.packedDataLen(data) + if chunkLength <= 1500 { + outData = outData[:authHeadLength+chunkLength] + } else { + newOutData := make([]byte, authHeadLength+chunkLength) + copy(newOutData, outData[:authHeadLength]) + outData = newOutData + } + a.packData(outData[authHeadLength:], data, randLength) + return +} + +func getRandStartPos(random *shift128PlusContext, randLength int) int { + if randLength > 0 { + return int(random.Next() % 8589934609 % uint64(randLength)) + } + return 0 +} + +func authChainAGetRandLen(dataLength int, random *shift128PlusContext, lastHash []byte, dataSizeList, dataSizeList2 []int, overhead int) int { + if dataLength > 1440 { + return 0 + } + random.InitFromBinDatalen(lastHash[:16], dataLength) + if dataLength > 1300 { + return int(random.Next() % 31) + } + if dataLength > 900 { + return int(random.Next() % 127) + } + if dataLength > 400 { + return int(random.Next() % 521) + } + return int(random.Next() % 1021) +} + +func udpGetRandLen(random *shift128PlusContext, lastHash []byte) int { + random.InitFromBin(lastHash[:16]) + return int(random.Next() % 127) +} + +type shift128PlusContext struct { + v [2]uint64 +} + +func (ctx *shift128PlusContext) InitFromBin(bin []byte) { + var fillBin [16]byte + copy(fillBin[:], bin) + + ctx.v[0] = binary.LittleEndian.Uint64(fillBin[:8]) + ctx.v[1] = binary.LittleEndian.Uint64(fillBin[8:]) +} + +func (ctx *shift128PlusContext) InitFromBinDatalen(bin []byte, datalen int) { + var fillBin [16]byte + copy(fillBin[:], bin) + binary.LittleEndian.PutUint16(fillBin[:2], uint16(datalen)) + + ctx.v[0] = binary.LittleEndian.Uint64(fillBin[:8]) + ctx.v[1] = binary.LittleEndian.Uint64(fillBin[8:]) + + for i := 0; i < 4; i++ { + ctx.Next() + } +} + +func (ctx *shift128PlusContext) Next() uint64 { + x := ctx.v[0] + y := ctx.v[1] + ctx.v[0] = y + x ^= x << 23 + x ^= y ^ (x >> 17) ^ (y >> 26) + ctx.v[1] = x + return x + y +} diff --git a/component/ssr/protocol/auth_chain_b.go b/component/ssr/protocol/auth_chain_b.go new file mode 100644 index 00000000..4f013929 --- /dev/null +++ b/component/ssr/protocol/auth_chain_b.go @@ -0,0 +1,72 @@ +package protocol + +import ( + "sort" + + "github.com/Dreamacro/clash/component/ssr/tools" +) + +func init() { + register("auth_chain_b", newAuthChainB) +} + +func newAuthChainB(b *Base) Protocol { + return &authChain{ + Base: b, + authData: &authData{}, + salt: "auth_chain_b", + hmac: tools.HmacMD5, + hashDigest: tools.SHA1Sum, + rnd: authChainBGetRandLen, + } +} + +func initDataSize(r *authChain) { + random := &r.randomServer + random.InitFromBin(r.Key) + len := random.Next()%8 + 4 + r.dataSizeList = make([]int, len) + for i := 0; i < int(len); i++ { + r.dataSizeList[i] = int(random.Next() % 2340 % 2040 % 1440) + } + sort.Ints(r.dataSizeList) + + len = random.Next()%16 + 8 + r.dataSizeList2 = make([]int, len) + for i := 0; i < int(len); i++ { + r.dataSizeList2[i] = int(random.Next() % 2340 % 2040 % 1440) + } + sort.Ints(r.dataSizeList2) +} + +func authChainBGetRandLen(dataLength int, random *shift128PlusContext, lastHash []byte, dataSizeList, dataSizeList2 []int, overhead int) int { + if dataLength > 1440 { + return 0 + } + random.InitFromBinDatalen(lastHash[:16], dataLength) + pos := sort.Search(len(dataSizeList), func(i int) bool { return dataSizeList[i] > dataLength+overhead }) + finalPos := uint64(pos) + random.Next()%uint64(len(dataSizeList)) + if finalPos < uint64(len(dataSizeList)) { + return dataSizeList[finalPos] - dataLength - overhead + } + + pos = sort.Search(len(dataSizeList2), func(i int) bool { return dataSizeList2[i] > dataLength+overhead }) + finalPos = uint64(pos) + random.Next()%uint64(len(dataSizeList2)) + if finalPos < uint64(len(dataSizeList2)) { + return dataSizeList2[finalPos] - dataLength - overhead + } + if finalPos < uint64(pos+len(dataSizeList2)-1) { + return 0 + } + + if dataLength > 1300 { + return int(random.Next() % 31) + } + if dataLength > 900 { + return int(random.Next() % 127) + } + if dataLength > 400 { + return int(random.Next() % 521) + } + return int(random.Next() % 1021) +} diff --git a/component/ssr/protocol/auth_sha1_v4.go b/component/ssr/protocol/auth_sha1_v4.go new file mode 100644 index 00000000..0cff1c86 --- /dev/null +++ b/component/ssr/protocol/auth_sha1_v4.go @@ -0,0 +1,253 @@ +package protocol + +import ( + "bytes" + "encoding/binary" + "hash/adler32" + "hash/crc32" + "math/rand" + "time" + + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/component/ssr/tools" +) + +type authSHA1V4 struct { + *Base + *authData + headerSent bool + buffer bytes.Buffer +} + +func init() { + register("auth_sha1_v4", newAuthSHA1V4) +} + +func newAuthSHA1V4(b *Base) Protocol { + return &authSHA1V4{Base: b, authData: &authData{}} +} + +func (a *authSHA1V4) initForConn(iv []byte) Protocol { + return &authSHA1V4{ + Base: &Base{ + IV: iv, + Key: a.Key, + TCPMss: a.TCPMss, + Overhead: a.Overhead, + Param: a.Param, + }, + authData: a.authData, + } +} + +func (a *authSHA1V4) GetProtocolOverhead() int { + return 7 +} + +func (a *authSHA1V4) SetOverhead(overhead int) { + a.Overhead = overhead +} + +func (a *authSHA1V4) Decode(b []byte) ([]byte, int, error) { + a.buffer.Reset() + bSize := len(b) + originalSize := bSize + for bSize > 4 { + crc := crc32.ChecksumIEEE(b[:2]) & 0xFFFF + if binary.LittleEndian.Uint16(b[2:4]) != uint16(crc) { + return nil, 0, errAuthSHA1v4CRC32Error + } + length := int(binary.BigEndian.Uint16(b[:2])) + if length >= 8192 || length < 8 { + return nil, 0, errAuthSHA1v4DataLengthError + } + if length > bSize { + break + } + + if adler32.Checksum(b[:length-4]) == binary.LittleEndian.Uint32(b[length-4:]) { + pos := int(b[4]) + if pos != 0xFF { + pos += 4 + } else { + pos = int(binary.BigEndian.Uint16(b[5:5+2])) + 4 + } + retSize := length - pos - 4 + a.buffer.Write(b[pos : pos+retSize]) + bSize -= length + b = b[length:] + } else { + return nil, 0, errAuthSHA1v4IncorrectChecksum + } + } + return a.buffer.Bytes(), originalSize - bSize, nil +} + +func (a *authSHA1V4) Encode(b []byte) ([]byte, error) { + a.buffer.Reset() + bSize := len(b) + offset := 0 + if !a.headerSent && bSize > 0 { + headSize := getHeadSize(b, 30) + if headSize > bSize { + headSize = bSize + } + a.buffer.Write(a.packAuthData(b[:headSize])) + offset += headSize + bSize -= headSize + a.headerSent = true + } + const blockSize = 4096 + for bSize > blockSize { + packSize, randSize := a.packedDataSize(b[offset : offset+blockSize]) + pack := pool.Get(packSize) + a.packData(b[offset:offset+blockSize], pack, randSize) + a.buffer.Write(pack) + pool.Put(pack) + offset += blockSize + bSize -= blockSize + } + if bSize > 0 { + packSize, randSize := a.packedDataSize(b[offset:]) + pack := pool.Get(packSize) + a.packData(b[offset:], pack, randSize) + a.buffer.Write(pack) + pool.Put(pack) + } + return a.buffer.Bytes(), nil +} + +func (a *authSHA1V4) DecodePacket(b []byte) ([]byte, int, error) { + return b, len(b), nil +} + +func (a *authSHA1V4) EncodePacket(b []byte) ([]byte, error) { + return b, nil +} + +func (a *authSHA1V4) packedDataSize(data []byte) (packSize, randSize int) { + dataSize := len(data) + randSize = 1 + if dataSize <= 1300 { + if dataSize > 400 { + randSize += rand.Intn(128) + } else { + randSize += rand.Intn(1024) + } + } + packSize = randSize + dataSize + 8 + return +} + +func (a *authSHA1V4) packData(data, ret []byte, randSize int) { + dataSize := len(data) + retSize := len(ret) + // 0~1, ret size + binary.BigEndian.PutUint16(ret[:2], uint16(retSize&0xFFFF)) + // 2~3, crc of ret size + crc := crc32.ChecksumIEEE(ret[:2]) & 0xFFFF + binary.LittleEndian.PutUint16(ret[2:4], uint16(crc)) + // 4, rand size + if randSize < 128 { + ret[4] = uint8(randSize & 0xFF) + } else { + ret[4] = uint8(0xFF) + binary.BigEndian.PutUint16(ret[5:7], uint16(randSize&0xFFFF)) + } + // (rand size+4)~(ret size-4), data + if dataSize > 0 { + copy(ret[randSize+4:], data) + } + // (ret size-4)~end, adler32 of full data + adler := adler32.Checksum(ret[:retSize-4]) + binary.LittleEndian.PutUint32(ret[retSize-4:], adler) +} + +func (a *authSHA1V4) packAuthData(data []byte) (ret []byte) { + dataSize := len(data) + randSize := 1 + if dataSize <= 1300 { + if dataSize > 400 { + randSize += rand.Intn(128) + } else { + randSize += rand.Intn(1024) + } + } + dataOffset := randSize + 4 + 2 + retSize := dataOffset + dataSize + 12 + tools.HmacSHA1Len + ret = make([]byte, retSize) + a.mutex.Lock() + defer a.mutex.Unlock() + a.connectionID++ + if a.connectionID > 0xFF000000 { + a.clientID = nil + } + if len(a.clientID) == 0 { + a.clientID = make([]byte, 8) + rand.Read(a.clientID) + b := make([]byte, 4) + rand.Read(b) + a.connectionID = binary.LittleEndian.Uint32(b) & 0xFFFFFF + } + // 0~1, ret size + binary.BigEndian.PutUint16(ret[:2], uint16(retSize&0xFFFF)) + + // 2~6, crc of (ret size+salt+key) + salt := []byte("auth_sha1_v4") + crcData := make([]byte, len(salt)+len(a.Key)+2) + copy(crcData[:2], ret[:2]) + copy(crcData[2:], salt) + copy(crcData[2+len(salt):], a.Key) + crc := crc32.ChecksumIEEE(crcData) & 0xFFFFFFFF + // 2~6, crc of (ret size+salt+key) + binary.LittleEndian.PutUint32(ret[2:], crc) + // 6~(rand size+6), rand numbers + rand.Read(ret[dataOffset-randSize : dataOffset]) + // 6, rand size + if randSize < 128 { + ret[6] = byte(randSize & 0xFF) + } else { + // 6, magic number 0xFF + ret[6] = 0xFF + // 7~8, rand size + binary.BigEndian.PutUint16(ret[7:9], uint16(randSize&0xFFFF)) + } + // rand size+6~(rand size+10), time stamp + now := time.Now().Unix() + binary.LittleEndian.PutUint32(ret[dataOffset:dataOffset+4], uint32(now)) + // rand size+10~(rand size+14), client ID + copy(ret[dataOffset+4:dataOffset+4+4], a.clientID[:4]) + // rand size+14~(rand size+18), connection ID + binary.LittleEndian.PutUint32(ret[dataOffset+8:dataOffset+8+4], a.connectionID) + // rand size+18~(rand size+18)+data length, data + copy(ret[dataOffset+12:], data) + + key := make([]byte, len(a.IV)+len(a.Key)) + copy(key, a.IV) + copy(key[len(a.IV):], a.Key) + + h := tools.HmacSHA1(key, ret[:retSize-tools.HmacSHA1Len]) + // (ret size-10)~(ret size)/(rand size)+18+data length~end, hmac + copy(ret[retSize-tools.HmacSHA1Len:], h[:tools.HmacSHA1Len]) + return ret +} + +func getHeadSize(data []byte, defaultValue int) int { + if data == nil || len(data) < 2 { + return defaultValue + } + headType := data[0] & 0x07 + switch headType { + case 1: + // IPv4 1+4+2 + return 7 + case 4: + // IPv6 1+16+2 + return 19 + case 3: + // domain name, variant length + return 4 + int(data[1]) + } + + return defaultValue +} diff --git a/component/ssr/protocol/base.go b/component/ssr/protocol/base.go new file mode 100644 index 00000000..ec7aad5d --- /dev/null +++ b/component/ssr/protocol/base.go @@ -0,0 +1,10 @@ +package protocol + +// Base information for protocol +type Base struct { + IV []byte + Key []byte + TCPMss int + Overhead int + Param string +} diff --git a/component/ssr/protocol/origin.go b/component/ssr/protocol/origin.go new file mode 100644 index 00000000..0d1fe217 --- /dev/null +++ b/component/ssr/protocol/origin.go @@ -0,0 +1,36 @@ +package protocol + +type origin struct{ *Base } + +func init() { + register("origin", newOrigin) +} + +func newOrigin(b *Base) Protocol { + return &origin{} +} + +func (o *origin) initForConn(iv []byte) Protocol { return &origin{} } + +func (o *origin) GetProtocolOverhead() int { + return 0 +} + +func (o *origin) SetOverhead(overhead int) { +} + +func (o *origin) Decode(b []byte) ([]byte, int, error) { + return b, len(b), nil +} + +func (o *origin) Encode(b []byte) ([]byte, error) { + return b, nil +} + +func (o *origin) DecodePacket(b []byte) ([]byte, int, error) { + return b, len(b), nil +} + +func (o *origin) EncodePacket(b []byte) ([]byte, error) { + return b, nil +} diff --git a/component/ssr/protocol/packet.go b/component/ssr/protocol/packet.go new file mode 100644 index 00000000..c3e5c702 --- /dev/null +++ b/component/ssr/protocol/packet.go @@ -0,0 +1,42 @@ +package protocol + +import ( + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +// NewPacketConn returns a net.NewPacketConn with protocol decoding/encoding +func NewPacketConn(pc net.PacketConn, p Protocol) net.PacketConn { + return &PacketConn{PacketConn: pc, Protocol: p.initForConn(nil)} +} + +// PacketConn represents a protocol packet connection +type PacketConn struct { + net.PacketConn + Protocol +} + +func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + buf := pool.Get(pool.RelayBufferSize) + defer pool.Put(buf) + buf, err := c.EncodePacket(b) + if err != nil { + return 0, err + } + _, err = c.PacketConn.WriteTo(buf, addr) + return len(b), err +} + +func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + n, addr, err := c.PacketConn.ReadFrom(b) + if err != nil { + return n, addr, err + } + bb, length, err := c.DecodePacket(b[:n]) + if err != nil { + return n, addr, err + } + copy(b, bb) + return length, addr, err +} diff --git a/component/ssr/protocol/protocol.go b/component/ssr/protocol/protocol.go new file mode 100644 index 00000000..b2aa8e93 --- /dev/null +++ b/component/ssr/protocol/protocol.go @@ -0,0 +1,61 @@ +package protocol + +import ( + "bytes" + "errors" + "fmt" + "strings" + "sync" +) + +var ( + errAuthAES128HMACError = errors.New("auth_aes128_* post decrypt hmac error") + errAuthAES128DataLengthError = errors.New("auth_aes128_* post decrypt length mismatch") + errAuthSHA1v4CRC32Error = errors.New("auth_sha1_v4 post decrypt data crc32 error") + errAuthSHA1v4DataLengthError = errors.New("auth_sha1_v4 post decrypt data length error") + errAuthSHA1v4IncorrectChecksum = errors.New("auth_sha1_v4 post decrypt incorrect checksum") + errAuthChainDataLengthError = errors.New("auth_chain_* post decrypt length mismatch") + errAuthChainHMACError = errors.New("auth_chain_* post decrypt hmac error") +) + +type authData struct { + clientID []byte + connectionID uint32 + mutex sync.Mutex +} + +type recvInfo struct { + recvID uint32 + buffer *bytes.Buffer +} + +type hmacMethod func(key []byte, data []byte) []byte +type hashDigestMethod func(data []byte) []byte +type rndMethod func(dataSize int, random *shift128PlusContext, lastHash []byte, dataSizeList, dataSizeList2 []int, overhead int) int + +// Protocol provides methods for decoding, encoding and iv setting +type Protocol interface { + initForConn(iv []byte) Protocol + GetProtocolOverhead() int + SetOverhead(int) + Decode([]byte) ([]byte, int, error) + Encode([]byte) ([]byte, error) + DecodePacket([]byte) ([]byte, int, error) + EncodePacket([]byte) ([]byte, error) +} + +type protocolCreator func(b *Base) Protocol + +var protocolList = make(map[string]protocolCreator) + +func register(name string, c protocolCreator) { + protocolList[name] = c +} + +// PickProtocol returns a protocol of the given name +func PickProtocol(name string, b *Base) (Protocol, error) { + if protocolCreator, ok := protocolList[strings.ToLower(name)]; ok { + return protocolCreator(b), nil + } + return nil, fmt.Errorf("Protocol %s not supported", name) +} diff --git a/component/ssr/protocol/stream.go b/component/ssr/protocol/stream.go new file mode 100644 index 00000000..c76d35eb --- /dev/null +++ b/component/ssr/protocol/stream.go @@ -0,0 +1,68 @@ +package protocol + +import ( + "bytes" + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +// NewConn wraps a stream-oriented net.Conn with protocol decoding/encoding +func NewConn(c net.Conn, p Protocol, iv []byte) net.Conn { + return &Conn{Conn: c, Protocol: p.initForConn(iv)} +} + +// Conn represents a protocol connection +type Conn struct { + net.Conn + Protocol + buf []byte + offset int + underDecoded bytes.Buffer +} + +func (c *Conn) Read(b []byte) (int, error) { + if c.buf != nil { + n := copy(b, c.buf[c.offset:]) + c.offset += n + if c.offset == len(c.buf) { + c.buf = nil + } + return n, nil + } + buf := pool.Get(pool.RelayBufferSize) + defer pool.Put(buf) + n, err := c.Conn.Read(buf) + if err != nil { + return 0, err + } + c.underDecoded.Write(buf[:n]) + underDecoded := c.underDecoded.Bytes() + decoded, length, err := c.Decode(underDecoded) + if err != nil { + c.underDecoded.Reset() + return 0, nil + } + if length == 0 { + return 0, nil + } + c.underDecoded.Next(length) + n = copy(b, decoded) + if len(decoded) > len(b) { + c.buf = decoded + c.offset = n + } + return n, nil +} + +func (c *Conn) Write(b []byte) (int, error) { + encoded, err := c.Encode(b) + if err != nil { + return 0, err + } + _, err = c.Conn.Write(encoded) + if err != nil { + return 0, err + } + return len(b), nil +} diff --git a/component/ssr/tools/encrypt.go b/component/ssr/tools/encrypt.go new file mode 100644 index 00000000..5fef1654 --- /dev/null +++ b/component/ssr/tools/encrypt.go @@ -0,0 +1,33 @@ +package tools + +import ( + "crypto/hmac" + "crypto/md5" + "crypto/sha1" +) + +const HmacSHA1Len = 10 + +func HmacMD5(key, data []byte) []byte { + hmacMD5 := hmac.New(md5.New, key) + hmacMD5.Write(data) + return hmacMD5.Sum(nil)[:16] +} + +func HmacSHA1(key, data []byte) []byte { + hmacSHA1 := hmac.New(sha1.New, key) + hmacSHA1.Write(data) + return hmacSHA1.Sum(nil)[:20] +} + +func MD5Sum(b []byte) []byte { + h := md5.New() + h.Write(b) + return h.Sum(nil) +} + +func SHA1Sum(b []byte) []byte { + h := sha1.New() + h.Write(b) + return h.Sum(nil) +} diff --git a/constant/adapters.go b/constant/adapters.go index ad80974a..a12975ba 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -13,6 +13,7 @@ const ( Reject Shadowsocks + ShadowsocksR Snell Socks5 Http @@ -100,6 +101,8 @@ func (at AdapterType) String() string { case Shadowsocks: return "Shadowsocks" + case ShadowsocksR: + return "ShadowsocksR" case Snell: return "Snell" case Socks5: diff --git a/go.mod b/go.mod index 90ecdf0a..d290d4c5 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Dreamacro/clash go 1.14 require ( - github.com/Dreamacro/go-shadowsocks2 v0.1.5 + github.com/Dreamacro/go-shadowsocks2 v0.1.6-0.20200722122336-8e5c7db4f96a github.com/eapache/queue v1.1.0 // indirect github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/cors v1.1.1 diff --git a/go.sum b/go.sum index a62815d4..1080e4b3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/Dreamacro/go-shadowsocks2 v0.1.5 h1:BizWSjmwzAyQoslz6YhJYMiAGT99j9cnm9zlxVr+kyI= -github.com/Dreamacro/go-shadowsocks2 v0.1.5/go.mod h1:LSXCjyHesPY3pLjhwff1mQX72ItcBT/N2xNC685cYeU= +github.com/Dreamacro/go-shadowsocks2 v0.1.6-0.20200722122336-8e5c7db4f96a h1:JhQFrFOkCpRB8qsN6PrzHFzjy/8iQpFFk5cbOiplh6s= +github.com/Dreamacro/go-shadowsocks2 v0.1.6-0.20200722122336-8e5c7db4f96a/go.mod h1:LSXCjyHesPY3pLjhwff1mQX72ItcBT/N2xNC685cYeU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=