diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6f9f50c8..e7616e3c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -103,11 +103,11 @@ jobs: prerelease: true generate_release_notes: true - - name: Delete workflow runs - uses: GitRML/delete-workflow-runs@main - with: - retain_days: 1 - keep_minimum_runs: 2 + #- name: Delete workflow runs + # uses: GitRML/delete-workflow-runs@main + # with: + # retain_days: 1 + # keep_minimum_runs: 2 - name: Remove old Releases uses: dev-drprasad/delete-older-releases@v0.2.0 diff --git a/README.md b/README.md index 4a50f50b..2b01fc9f 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ mitm: ``` ### DNS configuration -Support resolve ip with a proxy tunnel. +Support resolve ip with a proxy tunnel or interface. Support `geosite` with `fallback-filter`. @@ -119,8 +119,8 @@ Use `curl -X POST controllerip:port/cache/fakeip/flush` to flush persistence fak - https://doh.pub/dns-query - tls://223.5.5.5:853 fallback: + - 'tls://8.8.4.4:853#proxy or interface' - 'https://1.0.0.1/dns-query#Proxy' # append the proxy adapter name to the end of DNS URL with '#' prefix. - - 'tls://8.8.4.4:853#Proxy' fallback-filter: geoip: false geosite: @@ -325,7 +325,9 @@ Support `Trojan` with XTLS. Support relay `UDP` traffic. -Currently XTLS only supports TCP transport. +Support filtering proxy providers in proxy groups. + +Support custom http request header, prefix name and V2Ray subscription URL in proxy providers. ```yaml proxies: # VLESS @@ -381,6 +383,37 @@ proxy-groups: - ss1 - ss2 - ss3 + + - name: "filtering-proxy-providers" + type: url-test + url: "http://www.gstatic.com/generate_204" + interval: 300 + tolerance: 200 + # lazy: true + filter: "XXX" # a regular expression + use: + - provider1 + +proxy-providers: + provider1: + type: http + url: "url" # support V2Ray subscription URL + interval: 3600 + path: ./providers/provider1.yaml + # filter: "xxx" + # prefix-name: "XXX-" + header: # custom http request header + User-Agent: + - "Clash/v1.10.6" + # Accept: + # - 'application/vnd.github.v3.raw' + # Authorization: + # - ' token xxxxxxxxxxx' + health-check: + enable: false + interval: 1200 + # lazy: false # default value is true + url: http://www.gstatic.com/generate_204 ``` ### IPTABLES configuration diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index ba1d8252..81b6f689 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -10,11 +10,10 @@ import ( "github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/component/dialer" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/shadowsocks/core" obfs "github.com/Dreamacro/clash/transport/simple-obfs" "github.com/Dreamacro/clash/transport/socks5" v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin" - - "github.com/Dreamacro/go-shadowsocks2/core" ) type ShadowSocks struct { diff --git a/adapter/outbound/shadowsocksr.go b/adapter/outbound/shadowsocksr.go index 4951860f..4d7c684d 100644 --- a/adapter/outbound/shadowsocksr.go +++ b/adapter/outbound/shadowsocksr.go @@ -8,12 +8,11 @@ import ( "github.com/Dreamacro/clash/component/dialer" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/shadowsocks/core" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowstream" "github.com/Dreamacro/clash/transport/ssr/obfs" "github.com/Dreamacro/clash/transport/ssr/protocol" - - "github.com/Dreamacro/go-shadowsocks2/core" - "github.com/Dreamacro/go-shadowsocks2/shadowaead" - "github.com/Dreamacro/go-shadowsocks2/shadowstream" ) type ShadowSocksR struct { diff --git a/adapter/outboundgroup/parser.go b/adapter/outboundgroup/parser.go index 0ce957ef..ad83231b 100644 --- a/adapter/outboundgroup/parser.go +++ b/adapter/outboundgroup/parser.go @@ -3,6 +3,7 @@ package outboundgroup import ( "errors" "fmt" + "regexp" "github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/provider" @@ -29,6 +30,7 @@ type GroupCommonOption struct { Interval int `group:"interval,omitempty"` Lazy bool `group:"lazy,omitempty"` DisableUDP bool `group:"disable-udp,omitempty"` + Filter string `group:"filter,omitempty"` } func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) { @@ -37,10 +39,23 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide groupOption := &GroupCommonOption{ Lazy: true, } - if err := decoder.Decode(config, groupOption); err != nil { + + var ( + filterRegx *regexp.Regexp + err error + ) + + if err = decoder.Decode(config, groupOption); err != nil { return nil, errFormat } + if groupOption.Filter != "" { + filterRegx, err = regexp.Compile(groupOption.Filter) + if err != nil { + return nil, fmt.Errorf("invalid filter regex: %w", err) + } + } + if groupOption.Type == "" || groupOption.Name == "" { return nil, errFormat } @@ -90,7 +105,7 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide } if len(groupOption.Use) != 0 { - list, err := getProviders(providersMap, groupOption.Use) + list, err := getProviders(providersMap, groupOption, filterRegx) if err != nil { return nil, err } @@ -130,8 +145,13 @@ func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) { return ps, nil } -func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]types.ProxyProvider, error) { - var ps []types.ProxyProvider +func getProviders(mapping map[string]types.ProxyProvider, groupOption *GroupCommonOption, filterRegx *regexp.Regexp) ([]types.ProxyProvider, error) { + var ( + ps []types.ProxyProvider + list = groupOption.Use + groupName = groupOption.Name + ) + for _, name := range list { p, ok := mapping[name] if !ok { @@ -141,6 +161,27 @@ func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]type if p.VehicleType() == types.Compatible { return nil, fmt.Errorf("proxy group %s can't contains in `use`", name) } + + if filterRegx != nil { + var hc *provider.HealthCheck + if groupOption.Type == "select" || groupOption.Type == "relay" { + hc = provider.NewHealthCheck([]C.Proxy{}, "", 0, true) + } else { + if groupOption.URL == "" || groupOption.Interval == 0 { + return nil, errMissHealthCheck + } + hc = provider.NewHealthCheck([]C.Proxy{}, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy) + } + + if _, ok = mapping[groupName]; ok { + groupName += "->" + p.Name() + } + + pd := p.(*provider.ProxySetProvider) + p = provider.NewProxyFilterProvider(groupName, pd, hc, filterRegx) + pd.RegisterProvidersInUse(p) + } + ps = append(ps, p) } return ps, nil diff --git a/adapter/provider/healthcheck.go b/adapter/provider/healthcheck.go index 430225c4..0664771f 100644 --- a/adapter/provider/healthcheck.go +++ b/adapter/provider/healthcheck.go @@ -65,8 +65,13 @@ func (hc *HealthCheck) touch() { } func (hc *HealthCheck) check() { + proxies := hc.proxies + if len(proxies) == 0 { + return + } + b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10)) - for _, proxy := range hc.proxies { + for _, proxy := range proxies { p := proxy b.Go(p.Name(), func() (bool, error) { ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) diff --git a/adapter/provider/parser.go b/adapter/provider/parser.go index 3833dc7e..81c48f2f 100644 --- a/adapter/provider/parser.go +++ b/adapter/provider/parser.go @@ -20,13 +20,15 @@ type healthCheckSchema struct { } type proxyProviderSchema struct { - Type string `provider:"type"` - Path string `provider:"path"` - URL string `provider:"url,omitempty"` - Interval int `provider:"interval,omitempty"` - Filter string `provider:"filter,omitempty"` - HealthCheck healthCheckSchema `provider:"health-check,omitempty"` - ForceCertVerify bool `provider:"force-cert-verify,omitempty"` + Type string `provider:"type"` + Path string `provider:"path"` + URL string `provider:"url,omitempty"` + Interval int `provider:"interval,omitempty"` + Filter string `provider:"filter,omitempty"` + HealthCheck healthCheckSchema `provider:"health-check,omitempty"` + ForceCertVerify bool `provider:"force-cert-verify,omitempty"` + PrefixName string `provider:"prefix-name,omitempty"` + Header map[string][]string `provider:"header,omitempty"` } func ParseProxyProvider(name string, mapping map[string]any, forceCertVerify bool) (types.ProxyProvider, error) { @@ -59,12 +61,12 @@ func ParseProxyProvider(name string, mapping map[string]any, forceCertVerify boo case "file": vehicle = NewFileVehicle(path) case "http": - vehicle = NewHTTPVehicle(schema.URL, path) + vehicle = NewHTTPVehicle(schema.URL, path, schema.Header) default: return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type) } interval := time.Duration(uint(schema.Interval)) * time.Second filter := schema.Filter - return NewProxySetProvider(name, interval, filter, vehicle, hc, schema.ForceCertVerify) + return NewProxySetProvider(name, interval, filter, vehicle, hc, schema.ForceCertVerify, schema.PrefixName) } diff --git a/adapter/provider/provider.go b/adapter/provider/provider.go index 534d0021..571a60ff 100644 --- a/adapter/provider/provider.go +++ b/adapter/provider/provider.go @@ -9,6 +9,7 @@ import ( "time" "github.com/Dreamacro/clash/adapter" + "github.com/Dreamacro/clash/common/convert" C "github.com/Dreamacro/clash/constant" types "github.com/Dreamacro/clash/constant/provider" @@ -30,8 +31,9 @@ type ProxySetProvider struct { type proxySetProvider struct { *fetcher[[]C.Proxy] - proxies []C.Proxy - healthCheck *HealthCheck + proxies []C.Proxy + healthCheck *HealthCheck + providersInUse []types.ProxyProvider } func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { @@ -86,17 +88,22 @@ func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy { func (pp *proxySetProvider) setProxies(proxies []C.Proxy) { pp.proxies = proxies pp.healthCheck.setProxy(proxies) - if pp.healthCheck.auto() { - go pp.healthCheck.check() + + for _, use := range pp.providersInUse { + _ = use.Update() } } +func (pp *proxySetProvider) RegisterProvidersInUse(providers ...types.ProxyProvider) { + pp.providersInUse = append(pp.providersInUse, providers...) +} + func stopProxyProvider(pd *ProxySetProvider) { pd.healthCheck.close() _ = pd.fetcher.Destroy() } -func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck, forceCertVerify bool) (*ProxySetProvider, error) { +func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck, forceCertVerify bool, prefixName string) (*ProxySetProvider, error) { filterReg, err := regexp.Compile(filter) if err != nil { return nil, fmt.Errorf("invalid filter regex: %w", err) @@ -111,7 +118,7 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh healthCheck: hc, } - fetcher := newFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, filterReg, forceCertVerify), proxiesOnUpdate(pd)) + fetcher := newFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, filterReg, forceCertVerify, prefixName), proxiesOnUpdate(pd)) pd.fetcher = fetcher wrapper := &ProxySetProvider{pd} @@ -196,18 +203,114 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co return wrapper, nil } +// ProxyFilterProvider for filter provider +type ProxyFilterProvider struct { + *proxyFilterProvider +} + +type proxyFilterProvider struct { + name string + psd *ProxySetProvider + proxies []C.Proxy + filter *regexp.Regexp + healthCheck *HealthCheck +} + +func (pf *proxyFilterProvider) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]any{ + "name": pf.Name(), + "type": pf.Type().String(), + "vehicleType": pf.VehicleType().String(), + "proxies": pf.Proxies(), + }) +} + +func (pf *proxyFilterProvider) Name() string { + return pf.name +} + +func (pf *proxyFilterProvider) HealthCheck() { + pf.healthCheck.check() +} + +func (pf *proxyFilterProvider) Update() error { + var proxies []C.Proxy + if pf.filter != nil { + for _, proxy := range pf.psd.Proxies() { + if !pf.filter.MatchString(proxy.Name()) { + continue + } + proxies = append(proxies, proxy) + } + } else { + proxies = pf.psd.Proxies() + } + + pf.proxies = proxies + pf.healthCheck.setProxy(proxies) + return nil +} + +func (pf *proxyFilterProvider) Initial() error { + return nil +} + +func (pf *proxyFilterProvider) VehicleType() types.VehicleType { + return pf.psd.VehicleType() +} + +func (pf *proxyFilterProvider) Type() types.ProviderType { + return types.Proxy +} + +func (pf *proxyFilterProvider) Proxies() []C.Proxy { + return pf.proxies +} + +func (pf *proxyFilterProvider) ProxiesWithTouch() []C.Proxy { + pf.healthCheck.touch() + return pf.Proxies() +} + +func stopProxyFilterProvider(pf *ProxyFilterProvider) { + pf.healthCheck.close() +} + +func NewProxyFilterProvider(name string, psd *ProxySetProvider, hc *HealthCheck, filterRegx *regexp.Regexp) *ProxyFilterProvider { + pd := &proxyFilterProvider{ + psd: psd, + name: name, + healthCheck: hc, + filter: filterRegx, + } + + _ = pd.Update() + + if hc.auto() { + go hc.process() + } + + wrapper := &ProxyFilterProvider{pd} + runtime.SetFinalizer(wrapper, stopProxyFilterProvider) + return wrapper +} + func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) { return func(elm []C.Proxy) { pd.setProxies(elm) } } -func proxiesParseAndFilter(filter string, filterReg *regexp.Regexp, forceCertVerify bool) parser[[]C.Proxy] { +func proxiesParseAndFilter(filter string, filterReg *regexp.Regexp, forceCertVerify bool, prefixName string) parser[[]C.Proxy] { return func(buf []byte) ([]C.Proxy, error) { schema := &ProxySchema{} if err := yaml.Unmarshal(buf, schema); err != nil { - return nil, err + proxies, err1 := convert.ConvertsV2Ray(buf) + if err1 != nil { + return nil, fmt.Errorf("%s, %w", err.Error(), err1) + } + schema.Proxies = proxies } if schema.Proxies == nil { @@ -219,6 +322,11 @@ func proxiesParseAndFilter(filter string, filterReg *regexp.Regexp, forceCertVer if name, ok := mapping["name"]; ok && len(filter) > 0 && !filterReg.MatchString(name.(string)) { continue } + + if prefixName != "" { + mapping["name"] = prefixName + mapping["name"].(string) + } + proxy, err := adapter.ParseProxy(mapping, forceCertVerify) if err != nil { return nil, fmt.Errorf("proxy %d error: %w", idx, err) diff --git a/adapter/provider/vehicle.go b/adapter/provider/vehicle.go index 4f08c317..224e6f4f 100644 --- a/adapter/provider/vehicle.go +++ b/adapter/provider/vehicle.go @@ -9,7 +9,9 @@ import ( "os" "time" + "github.com/Dreamacro/clash/common/convert" "github.com/Dreamacro/clash/component/dialer" + C "github.com/Dreamacro/clash/constant" types "github.com/Dreamacro/clash/constant/provider" ) @@ -34,8 +36,9 @@ func NewFileVehicle(path string) *FileVehicle { } type HTTPVehicle struct { - url string - path string + url string + path string + header http.Header } func (h *HTTPVehicle) Type() types.VehicleType { @@ -60,11 +63,19 @@ func (h *HTTPVehicle) Read() ([]byte, error) { return nil, err } + if h.header != nil { + req.Header = h.header + } + if user := uri.User; user != nil { password, _ := user.Password() req.SetBasicAuth(user.Username(), password) } + if req.UserAgent() == "" { + convert.SetUserAgent(req) + } + req = req.WithContext(ctx) transport := &http.Transport{ @@ -73,8 +84,13 @@ func (h *HTTPVehicle) Read() ([]byte, error) { IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, - DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - return dialer.DialContext(ctx, network, address) + DialContext: func(ctx context.Context, network, address string) (conn net.Conn, err error) { + conn, err = dialer.DialContext(ctx, network, address) // with direct + if err != nil { + // fallback to tun if tun enabled + conn, err = (&net.Dialer{Timeout: C.DefaultTCPTimeout}).Dial(network, address) + } + return }, } @@ -83,7 +99,9 @@ func (h *HTTPVehicle) Read() ([]byte, error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() buf, err := io.ReadAll(resp.Body) if err != nil { @@ -93,6 +111,6 @@ func (h *HTTPVehicle) Read() ([]byte, error) { return buf, nil } -func NewHTTPVehicle(url string, path string) *HTTPVehicle { - return &HTTPVehicle{url, path} +func NewHTTPVehicle(url string, path string, header http.Header) *HTTPVehicle { + return &HTTPVehicle{url, path, header} } diff --git a/common/convert/converter.go b/common/convert/converter.go new file mode 100644 index 00000000..c9b239a9 --- /dev/null +++ b/common/convert/converter.go @@ -0,0 +1,344 @@ +package convert + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "strings" +) + +var enc = base64.StdEncoding + +func DecodeBase64(buf []byte) ([]byte, error) { + dBuf := make([]byte, enc.DecodedLen(len(buf))) + n, err := enc.Decode(dBuf, buf) + if err != nil { + return nil, err + } + + return dBuf[:n], nil +} + +// DecodeBase64StringToString decode base64 string to string +func DecodeBase64StringToString(s string) (string, error) { + dBuf, err := enc.DecodeString(s) + if err != nil { + return "", err + } + + return string(dBuf), nil +} + +// ConvertsV2Ray convert V2Ray subscribe proxies data to clash proxies config +func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { + data, err := DecodeBase64(buf) + if err != nil { + data = buf + } + + arr := strings.Split(string(data), "\n") + + proxies := make([]map[string]any, 0, len(arr)) + names := make(map[string]int, 200) + + for _, line := range arr { + line = strings.TrimRight(line, " \r") + if line == "" { + continue + } + + scheme, body, found := strings.Cut(line, "://") + if !found { + continue + } + + scheme = strings.ToLower(scheme) + switch scheme { + case "trojan": + urlTrojan, err := url.Parse(line) + if err != nil { + continue + } + + q := urlTrojan.Query() + + name := uniqueName(names, urlTrojan.Fragment) + trojan := make(map[string]any, 20) + + trojan["name"] = name + trojan["type"] = scheme + trojan["server"] = urlTrojan.Hostname() + trojan["port"] = urlTrojan.Port() + trojan["password"] = urlTrojan.User.Username() + trojan["udp"] = true + trojan["skip-cert-verify"] = false + + sni := q.Get("sni") + if sni != "" { + trojan["sni"] = sni + } + + network := strings.ToLower(q.Get("type")) + if network != "" { + trojan["network"] = network + } + + if network == "ws" { + headers := make(map[string]any) + wsOpts := make(map[string]any) + + headers["Host"] = RandHost() + headers["User-Agent"] = RandUserAgent() + + wsOpts["path"] = q.Get("path") + wsOpts["headers"] = headers + + trojan["ws-opts"] = wsOpts + } + + proxies = append(proxies, trojan) + case "vmess": + dcBuf, err := enc.DecodeString(body) + if err != nil { + continue + } + + jsonDc := json.NewDecoder(bytes.NewReader(dcBuf)) + values := make(map[string]any, 20) + + if jsonDc.Decode(&values) != nil { + continue + } + + name := uniqueName(names, values["ps"].(string)) + vmess := make(map[string]any, 20) + + vmess["name"] = name + vmess["type"] = scheme + vmess["server"] = values["add"] + vmess["port"] = values["port"] + vmess["uuid"] = values["id"] + vmess["alterId"] = values["aid"] + vmess["cipher"] = "auto" + vmess["udp"] = true + vmess["skip-cert-verify"] = false + + host := values["host"] + network := strings.ToLower(values["net"].(string)) + + vmess["network"] = network + + tls := strings.ToLower(values["tls"].(string)) + if tls != "" && tls != "0" && tls != "null" { + if host != nil { + vmess["servername"] = host + } + vmess["tls"] = true + } + + if network == "ws" { + headers := make(map[string]any) + wsOpts := make(map[string]any) + + headers["Host"] = RandHost() + headers["User-Agent"] = RandUserAgent() + + if values["path"] != nil { + wsOpts["path"] = values["path"] + } + wsOpts["headers"] = headers + + vmess["ws-opts"] = wsOpts + } + + proxies = append(proxies, vmess) + case "ss": + urlSS, err := url.Parse(line) + if err != nil { + continue + } + + name := uniqueName(names, urlSS.Fragment) + port := urlSS.Port() + + if port == "" { + dcBuf, err := enc.DecodeString(urlSS.Host) + if err != nil { + continue + } + + urlSS, err = url.Parse("ss://" + string(dcBuf)) + if err != nil { + continue + } + } + + var ( + cipher = urlSS.User.Username() + password string + ) + + if password, found = urlSS.User.Password(); !found { + dcBuf, err := enc.DecodeString(cipher) + if err != nil { + continue + } + + cipher, password, found = strings.Cut(string(dcBuf), ":") + if !found { + continue + } + } + + ss := make(map[string]any, 20) + + ss["name"] = name + ss["type"] = scheme + ss["server"] = urlSS.Hostname() + ss["port"] = urlSS.Port() + ss["cipher"] = cipher + ss["password"] = password + ss["udp"] = true + + proxies = append(proxies, ss) + case "ssr": + dcBuf, err := enc.DecodeString(body) + if err != nil { + continue + } + + // ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64&protoparam=&remarks=urlsafebase64&group=urlsafebase64&udpport=0&uot=1 + + before, after, ok := strings.Cut(string(dcBuf), "/?") + if !ok { + continue + } + + beforeArr := strings.Split(before, ":") + + if len(beforeArr) != 6 { + continue + } + + host := beforeArr[0] + port := beforeArr[1] + protocol := beforeArr[2] + method := beforeArr[3] + obfs := beforeArr[4] + password := decodeUrlSafe(urlSafe(beforeArr[5])) + + query, err := url.ParseQuery(urlSafe(after)) + if err != nil { + continue + } + + remarks := decodeUrlSafe(query.Get("remarks")) + name := uniqueName(names, remarks) + + obfsParam := decodeUrlSafe(query.Get("obfsparam")) + protocolParam := query.Get("protoparam") + + ssr := make(map[string]any, 20) + + ssr["name"] = name + ssr["type"] = scheme + ssr["server"] = host + ssr["port"] = port + ssr["cipher"] = method + ssr["password"] = password + ssr["obfs"] = obfs + ssr["protocol"] = protocol + ssr["udp"] = true + + if obfsParam != "" { + ssr["obfs-param"] = obfsParam + } + + if protocolParam != "" { + ssr["protocol-param"] = protocolParam + } + + proxies = append(proxies, ssr) + case "vless": + urlVless, err := url.Parse(line) + if err != nil { + continue + } + + q := urlVless.Query() + + name := uniqueName(names, urlVless.Fragment) + vless := make(map[string]any, 20) + + vless["name"] = name + vless["type"] = scheme + vless["server"] = urlVless.Hostname() + vless["port"] = urlVless.Port() + vless["uuid"] = urlVless.User.Username() + vless["udp"] = true + vless["skip-cert-verify"] = false + + sni := q.Get("sni") + if sni != "" { + vless["servername"] = sni + } + + flow := strings.ToLower(q.Get("flow")) + if flow != "" { + vless["flow"] = flow + } + + network := strings.ToLower(q.Get("type")) + if network != "" { + vless["network"] = network + } + + if network == "ws" { + headers := make(map[string]any) + wsOpts := make(map[string]any) + + headers["Host"] = RandHost() + headers["User-Agent"] = RandUserAgent() + + wsOpts["path"] = q.Get("path") + wsOpts["headers"] = headers + + vless["ws-opts"] = wsOpts + } + + proxies = append(proxies, vless) + } + } + + if len(proxies) == 0 { + return nil, fmt.Errorf("convert v2ray subscribe error: format invalid") + } + + return proxies, nil +} + +func urlSafe(data string) string { + return strings.ReplaceAll(strings.ReplaceAll(data, "+", "-"), "/", "_") +} + +func decodeUrlSafe(data string) string { + dcBuf, err := base64.URLEncoding.DecodeString(data) + if err != nil { + return "" + } + return string(dcBuf) +} + +func uniqueName(names map[string]int, name string) string { + if index, ok := names[name]; ok { + index++ + names[name] = index + name = name + "-" + fmt.Sprintf("%02d", index) + } else { + index = 0 + names[name] = index + } + return name +} diff --git a/common/convert/util.go b/common/convert/util.go new file mode 100644 index 00000000..d80c278d --- /dev/null +++ b/common/convert/util.go @@ -0,0 +1,313 @@ +package convert + +import ( + "encoding/base64" + "math/rand" + "net/http" + "strings" + + "github.com/gofrs/uuid" +) + +var hostsSuffix = []string{ + "-cdn.aliyuncs.com", + ".alicdn.com", + ".pan.baidu.com", + ".tbcache.com", + ".aliyuncdn.com", + ".vod.miguvideo.com", + ".cibntv.net", + ".myqcloud.com", + ".smtcdns.com", + ".alikunlun.com", + ".smtcdns.net", + ".apcdns.net", + ".cdn-go.cn", + ".cdntip.com", + ".cdntips.com", + ".alidayu.com", + ".alidns.com", + ".cdngslb.com", + ".mxhichina.com", + ".alibabadns.com", +} + +var userAgents = []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", +} + +var ( + hostsLen = len(hostsSuffix) + uaLen = len(userAgents) +) + +func RandHost() string { + id, _ := uuid.NewV4() + base := strings.ToLower(base64.RawURLEncoding.EncodeToString(id.Bytes())) + base = strings.ReplaceAll(base, "-", "") + base = strings.ReplaceAll(base, "_", "") + buf := []byte(base) + prefix := string(buf[:3]) + "---" + prefix += string(buf[6:8]) + "-" + prefix += string(buf[len(buf)-8:]) + + return prefix + hostsSuffix[rand.Intn(hostsLen)] +} + +func RandUserAgent() string { + return userAgents[rand.Intn(uaLen)] +} + +func SetUserAgent(req *http.Request) { + userAgent := RandUserAgent() + req.Header.Set("User-Agent", userAgent) +} diff --git a/config/config.go b/config/config.go index cf509a30..0a3114e7 100644 --- a/config/config.go +++ b/config/config.go @@ -456,7 +456,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[ } for _, proxyProvider := range providersMap { - log.Infoln("Start initial provider %s", proxyProvider.Name()) + log.Infoln("Start initial proxy provider %s", proxyProvider.Name()) if err := proxyProvider.Initial(); err != nil { return nil, nil, fmt.Errorf("initial proxy provider %s error: %w", proxyProvider.Name(), err) } diff --git a/config/initial.go b/config/initial.go index 2d816805..3f082905 100644 --- a/config/initial.go +++ b/config/initial.go @@ -6,13 +6,14 @@ import ( "net/http" "os" + "github.com/Dreamacro/clash/common/convert" "github.com/Dreamacro/clash/component/mmdb" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/log" ) func downloadMMDB(path string) (err error) { - resp, err := http.Get("https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb") + resp, err := doGet("https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb") if err != nil { return } @@ -51,7 +52,7 @@ func initMMDB() error { } func downloadGeoSite(path string) (err error) { - resp, err := http.Get("https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat") + resp, err := doGet("https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat") if err != nil { return } @@ -110,3 +111,16 @@ func Init(dir string) error { } return nil } + +func doGet(url string) (resp *http.Response, err error) { + var req *http.Request + req, err = http.NewRequest("GET", url, nil) + if err != nil { + return + } + + convert.SetUserAgent(req) + + resp, err = http.DefaultClient.Do(req) + return +} diff --git a/dns/client.go b/dns/client.go index b536f0b1..4795e58c 100644 --- a/dns/client.go +++ b/dns/client.go @@ -54,10 +54,14 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) } var conn net.Conn - if c.proxyAdapter == "" { - conn, err = dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), c.port), options...) - } else { + if c.proxyAdapter != "" { conn, err = dialContextWithProxyAdapter(ctx, c.proxyAdapter, network, ip, c.port, options...) + if err == errProxyNotFound { + options = append(options[:0], dialer.WithInterface(c.proxyAdapter), dialer.WithRoutingMark(0)) + conn, err = dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), c.port), options...) + } + } else { + conn, err = dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), c.port), options...) } if err != nil { diff --git a/dns/doh.go b/dns/doh.go index 51352a0e..28c23164 100644 --- a/dns/doh.go +++ b/dns/doh.go @@ -69,7 +69,9 @@ func (dc *dohClient) doRequest(req *http.Request) (msg *D.Msg, err error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() buf, err := io.ReadAll(resp.Body) if err != nil { @@ -97,11 +99,17 @@ func newDoHClient(url string, r *Resolver, proxyAdapter string) *dohClient { return nil, err } - if proxyAdapter == "" { - return dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), port)) - } else { - return dialContextWithProxyAdapter(ctx, proxyAdapter, "tcp", ip, port) + if proxyAdapter != "" { + var conn net.Conn + conn, err = dialContextWithProxyAdapter(ctx, proxyAdapter, "tcp", ip, port) + if err == errProxyNotFound { + options := []dialer.Option{dialer.WithInterface(proxyAdapter), dialer.WithRoutingMark(0)} + conn, err = dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), port), options...) + } + return conn, err } + + return dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), port)) }, }, } diff --git a/dns/util.go b/dns/util.go index 515bb88f..f573fe7a 100644 --- a/dns/util.go +++ b/dns/util.go @@ -3,6 +3,7 @@ package dns import ( "context" "crypto/tls" + "errors" "fmt" "net" "net/netip" @@ -19,6 +20,8 @@ import ( D "github.com/miekg/dns" ) +var errProxyNotFound = errors.New("proxy adapter not found") + func putMsgToCache(c *cache.LruCache[string, *D.Msg], key string, msg *D.Msg) { var ttl uint32 switch { @@ -135,7 +138,7 @@ func (wpc *wrapPacketConn) RemoteAddr() net.Addr { func dialContextWithProxyAdapter(ctx context.Context, adapterName string, network string, dstIP netip.Addr, port string, opts ...dialer.Option) (net.Conn, error) { proxy, ok := tunnel.Proxies()[adapterName] if !ok { - return nil, fmt.Errorf("proxy adapter [%s] not found", adapterName) + return nil, errProxyNotFound } networkType := C.TCP diff --git a/go.mod b/go.mod index 30e458ad..68d98212 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/Dreamacro/clash go 1.18 require ( - github.com/Dreamacro/go-shadowsocks2 v0.1.8 github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/cors v1.2.1 github.com/go-chi/render v1.0.1 @@ -19,17 +18,17 @@ require ( go.uber.org/atomic v1.9.0 go.uber.org/automaxprocs v1.5.1 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e - golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf - golang.org/x/net v0.0.0-20220526153639-5463443f8c37 - golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 + golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f + golang.org/x/net v0.0.0-20220531201128-c960675eff93 + golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab golang.org/x/time v0.0.0-20220411224347-583f2d630306 - golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d + golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4 golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e google.golang.org/protobuf v1.28.0 gopkg.in/yaml.v3 v3.0.1 - gvisor.dev/gvisor v0.0.0-20220527053002-8ab279227ac8 + gvisor.dev/gvisor v0.0.0-20220601233344-46e478629075 ) require ( diff --git a/go.sum b/go.sum index 5f6eb917..01a449a3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/Dreamacro/go-shadowsocks2 v0.1.8 h1:Ixejp5JscEc866gAvm/l6TFd7BOBvDviKgwb1quWw3g= -github.com/Dreamacro/go-shadowsocks2 v0.1.8/go.mod h1:51y4Q6tJoCE7e8TmYXcQRqfoxPfE9Cvn79V6pB6Df7Y= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= @@ -82,8 +80,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf h1:oXVg4h2qJDd9htKxb5SCpFBHLipW6hXmL3qpUixS2jw= -golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys= +golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f h1:KK6mxegmt5hGJRcAnEDjSNLxIRhZxDcgwMbcO/lMCRM= +golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= @@ -99,12 +97,12 @@ golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8= -golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA= +golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -148,8 +146,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= -golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d h1:q4JksJ2n0fmbXC0Aj0eOs6E0AcPqnKglxWXWFqGD6x0= -golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U= +golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4 h1:QlbNZ9SwDAepRQwgeWHLi3rfEMo/kVEU4SmgsNM7HmQ= +golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U= golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e h1:yV04h6Tx19uDR6LvuEbR19cDU+3QrB9LuGjtF7F5G0w= golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e/go.mod h1:1CeiatTZwcwSFA3cAtMm8CQoroviTldnxd7DOgM/vI4= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= @@ -160,5 +158,5 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gvisor.dev/gvisor v0.0.0-20220527053002-8ab279227ac8 h1:K6RgHqNR+9t3sKVsfRFsvXryRL5kL6wtBPU5aPt1jLY= -gvisor.dev/gvisor v0.0.0-20220527053002-8ab279227ac8/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM= +gvisor.dev/gvisor v0.0.0-20220601233344-46e478629075 h1:ucwwit0X39HmMQ1iSeNCIXw4g/B8bi+O6TSxxDbPs9E= +gvisor.dev/gvisor v0.0.0-20220601233344-46e478629075/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM= diff --git a/hub/route/configsGeo.go b/hub/route/configsGeo.go index a38ab03a..f64995ed 100644 --- a/hub/route/configsGeo.go +++ b/hub/route/configsGeo.go @@ -49,14 +49,14 @@ func updateGeoDatabases(w http.ResponseWriter, r *http.Request) { return } + log.Warnln("[REST-API] update GEO databases successful, apply config...") + cfg, err := executor.ParseWithPath(constant.Path.Config()) if err != nil { log.Errorln("[REST-API] update GEO databases failed: %v", err) return } - log.Warnln("[REST-API] update GEO databases successful, apply config...") - executor.ApplyConfig(cfg, false) }() diff --git a/hub/route/proxies.go b/hub/route/proxies.go index bba9e2a8..23918901 100644 --- a/hub/route/proxies.go +++ b/hub/route/proxies.go @@ -11,6 +11,7 @@ import ( "github.com/Dreamacro/clash/adapter/outboundgroup" "github.com/Dreamacro/clash/component/profile/cachefile" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/tunnel" "github.com/go-chi/chi/v5" @@ -43,6 +44,10 @@ func findProxyByName(next http.Handler) http.Handler { name := r.Context().Value(CtxKeyProxyName).(string) proxies := tunnel.Proxies() proxy, exist := proxies[name] + if !exist { + proxy, exist = findProxyInNonCompatibleProviderByName(name) + } + if !exist { render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) @@ -128,3 +133,20 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) { "delay": delay, }) } + +func findProxyInNonCompatibleProviderByName(name string) (proxy C.Proxy, found bool) { + providers := tunnel.Providers() + for _, pd := range providers { + if pd.VehicleType() == provider.Compatible { + continue + } + for _, pp := range pd.Proxies() { + found = pp.Name() == name + if found { + proxy = pp + return + } + } + } + return +} diff --git a/test/go.mod b/test/go.mod index a915c1db..56f0612b 100644 --- a/test/go.mod +++ b/test/go.mod @@ -8,13 +8,12 @@ require ( github.com/docker/go-connections v0.4.0 github.com/miekg/dns v1.1.49 github.com/stretchr/testify v1.7.1 - golang.org/x/net v0.0.0-20220526153639-5463443f8c37 + golang.org/x/net v0.0.0-20220531201128-c960675eff93 ) replace github.com/Dreamacro/clash => ../ require ( - github.com/Dreamacro/go-shadowsocks2 v0.1.8 // indirect github.com/Microsoft/go-winio v0.5.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/distribution v2.8.1+incompatible // indirect @@ -38,20 +37,20 @@ require ( go.etcd.io/bbolt v1.3.6 // indirect go.uber.org/atomic v1.9.0 // indirect golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect - golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf // indirect + golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f // indirect golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect - golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect + golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect golang.org/x/tools v0.1.10 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect - golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d // indirect + golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4 // indirect golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.1.0 // indirect - gvisor.dev/gvisor v0.0.0-20220527053002-8ab279227ac8 // indirect + gvisor.dev/gvisor v0.0.0-20220601233344-46e478629075 // indirect ) diff --git a/test/go.sum b/test/go.sum index f468eb29..83f64669 100644 --- a/test/go.sum +++ b/test/go.sum @@ -1,7 +1,5 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Dreamacro/go-shadowsocks2 v0.1.8 h1:Ixejp5JscEc866gAvm/l6TFd7BOBvDviKgwb1quWw3g= -github.com/Dreamacro/go-shadowsocks2 v0.1.8/go.mod h1:51y4Q6tJoCE7e8TmYXcQRqfoxPfE9Cvn79V6pB6Df7Y= github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -103,8 +101,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf h1:oXVg4h2qJDd9htKxb5SCpFBHLipW6hXmL3qpUixS2jw= -golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys= +golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f h1:KK6mxegmt5hGJRcAnEDjSNLxIRhZxDcgwMbcO/lMCRM= +golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -124,14 +122,14 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8= -golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA= +golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -182,8 +180,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= -golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d h1:q4JksJ2n0fmbXC0Aj0eOs6E0AcPqnKglxWXWFqGD6x0= -golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U= +golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4 h1:QlbNZ9SwDAepRQwgeWHLi3rfEMo/kVEU4SmgsNM7HmQ= +golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U= golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e h1:yV04h6Tx19uDR6LvuEbR19cDU+3QrB9LuGjtF7F5G0w= golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e/go.mod h1:1CeiatTZwcwSFA3cAtMm8CQoroviTldnxd7DOgM/vI4= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= @@ -198,5 +196,5 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= -gvisor.dev/gvisor v0.0.0-20220527053002-8ab279227ac8 h1:K6RgHqNR+9t3sKVsfRFsvXryRL5kL6wtBPU5aPt1jLY= -gvisor.dev/gvisor v0.0.0-20220527053002-8ab279227ac8/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM= +gvisor.dev/gvisor v0.0.0-20220601233344-46e478629075 h1:ucwwit0X39HmMQ1iSeNCIXw4g/B8bi+O6TSxxDbPs9E= +gvisor.dev/gvisor v0.0.0-20220601233344-46e478629075/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM= diff --git a/transport/shadowsocks/README.md b/transport/shadowsocks/README.md new file mode 100644 index 00000000..c9fc815c --- /dev/null +++ b/transport/shadowsocks/README.md @@ -0,0 +1,5 @@ +## Embedded go-shadowsocks2 + +from https://github.com/Dreamacro/go-shadowsocks2 + +origin https://github.com/riobard/go-shadowsocks2 diff --git a/transport/shadowsocks/core/cipher.go b/transport/shadowsocks/core/cipher.go new file mode 100644 index 00000000..2f5acf63 --- /dev/null +++ b/transport/shadowsocks/core/cipher.go @@ -0,0 +1,164 @@ +package core + +import ( + "crypto/md5" + "errors" + "net" + "sort" + "strings" + + "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowstream" +) + +type Cipher interface { + StreamConnCipher + PacketConnCipher +} + +type StreamConnCipher interface { + StreamConn(net.Conn) net.Conn +} + +type PacketConnCipher interface { + PacketConn(net.PacketConn) net.PacketConn +} + +// ErrCipherNotSupported occurs when a cipher is not supported (likely because of security concerns). +var ErrCipherNotSupported = errors.New("cipher not supported") + +const ( + aeadAes128Gcm = "AEAD_AES_128_GCM" + aeadAes192Gcm = "AEAD_AES_192_GCM" + aeadAes256Gcm = "AEAD_AES_256_GCM" + aeadChacha20Poly1305 = "AEAD_CHACHA20_POLY1305" + aeadXChacha20Poly1305 = "AEAD_XCHACHA20_POLY1305" +) + +// List of AEAD ciphers: key size in bytes and constructor +var aeadList = map[string]struct { + KeySize int + New func([]byte) (shadowaead.Cipher, error) +}{ + aeadAes128Gcm: {16, shadowaead.AESGCM}, + aeadAes192Gcm: {24, shadowaead.AESGCM}, + aeadAes256Gcm: {32, shadowaead.AESGCM}, + aeadChacha20Poly1305: {32, shadowaead.Chacha20Poly1305}, + aeadXChacha20Poly1305: {32, shadowaead.XChacha20Poly1305}, +} + +// List of stream ciphers: key size in bytes and constructor +var streamList = map[string]struct { + KeySize int + New func(key []byte) (shadowstream.Cipher, error) +}{ + "RC4-MD5": {16, shadowstream.RC4MD5}, + "AES-128-CTR": {16, shadowstream.AESCTR}, + "AES-192-CTR": {24, shadowstream.AESCTR}, + "AES-256-CTR": {32, shadowstream.AESCTR}, + "AES-128-CFB": {16, shadowstream.AESCFB}, + "AES-192-CFB": {24, shadowstream.AESCFB}, + "AES-256-CFB": {32, shadowstream.AESCFB}, + "CHACHA20-IETF": {32, shadowstream.Chacha20IETF}, + "XCHACHA20": {32, shadowstream.Xchacha20}, +} + +// ListCipher returns a list of available cipher names sorted alphabetically. +func ListCipher() []string { + var l []string + for k := range aeadList { + l = append(l, k) + } + for k := range streamList { + l = append(l, k) + } + sort.Strings(l) + return l +} + +// PickCipher returns a Cipher of the given name. Derive key from password if given key is empty. +func PickCipher(name string, key []byte, password string) (Cipher, error) { + name = strings.ToUpper(name) + + switch name { + case "DUMMY": + return &dummy{}, nil + case "CHACHA20-IETF-POLY1305": + name = aeadChacha20Poly1305 + case "XCHACHA20-IETF-POLY1305": + name = aeadXChacha20Poly1305 + case "AES-128-GCM": + name = aeadAes128Gcm + case "AES-192-GCM": + name = aeadAes192Gcm + case "AES-256-GCM": + name = aeadAes256Gcm + } + + if choice, ok := aeadList[name]; ok { + if len(key) == 0 { + key = Kdf(password, choice.KeySize) + } + if len(key) != choice.KeySize { + return nil, shadowaead.KeySizeError(choice.KeySize) + } + aead, err := choice.New(key) + return &AeadCipher{Cipher: aead, Key: key}, err + } + + if choice, ok := streamList[name]; ok { + if len(key) == 0 { + key = Kdf(password, choice.KeySize) + } + if len(key) != choice.KeySize { + return nil, shadowstream.KeySizeError(choice.KeySize) + } + ciph, err := choice.New(key) + return &StreamCipher{Cipher: ciph, Key: key}, err + } + + return nil, ErrCipherNotSupported +} + +type AeadCipher struct { + shadowaead.Cipher + + Key []byte +} + +func (aead *AeadCipher) StreamConn(c net.Conn) net.Conn { return shadowaead.NewConn(c, aead) } +func (aead *AeadCipher) PacketConn(c net.PacketConn) net.PacketConn { + return shadowaead.NewPacketConn(c, aead) +} + +type StreamCipher struct { + shadowstream.Cipher + + Key []byte +} + +func (ciph *StreamCipher) StreamConn(c net.Conn) net.Conn { return shadowstream.NewConn(c, ciph) } +func (ciph *StreamCipher) PacketConn(c net.PacketConn) net.PacketConn { + return shadowstream.NewPacketConn(c, ciph) +} + +// dummy cipher does not encrypt + +type dummy struct{} + +func (dummy) StreamConn(c net.Conn) net.Conn { return c } +func (dummy) PacketConn(c net.PacketConn) net.PacketConn { return c } + +// key-derivation function from original Shadowsocks +func Kdf(password string, keyLen int) []byte { + var b, prev []byte + h := md5.New() + for len(b) < keyLen { + h.Write(prev) + h.Write([]byte(password)) + b = h.Sum(b) + prev = b[len(b)-h.Size():] + h.Reset() + } + return b[:keyLen] +} diff --git a/transport/shadowsocks/shadowaead/cipher.go b/transport/shadowsocks/shadowaead/cipher.go new file mode 100644 index 00000000..3cf75749 --- /dev/null +++ b/transport/shadowsocks/shadowaead/cipher.go @@ -0,0 +1,94 @@ +package shadowaead + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha1" + "io" + "strconv" + + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/hkdf" +) + +type Cipher interface { + KeySize() int + SaltSize() int + Encrypter(salt []byte) (cipher.AEAD, error) + Decrypter(salt []byte) (cipher.AEAD, error) +} + +type KeySizeError int + +func (e KeySizeError) Error() string { + return "key size error: need " + strconv.Itoa(int(e)) + " bytes" +} + +func hkdfSHA1(secret, salt, info, outkey []byte) { + r := hkdf.New(sha1.New, secret, salt, info) + if _, err := io.ReadFull(r, outkey); err != nil { + panic(err) // should never happen + } +} + +type metaCipher struct { + psk []byte + makeAEAD func(key []byte) (cipher.AEAD, error) +} + +func (a *metaCipher) KeySize() int { return len(a.psk) } +func (a *metaCipher) SaltSize() int { + if ks := a.KeySize(); ks > 16 { + return ks + } + return 16 +} + +func (a *metaCipher) Encrypter(salt []byte) (cipher.AEAD, error) { + subkey := make([]byte, a.KeySize()) + hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey) + return a.makeAEAD(subkey) +} + +func (a *metaCipher) Decrypter(salt []byte) (cipher.AEAD, error) { + subkey := make([]byte, a.KeySize()) + hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey) + return a.makeAEAD(subkey) +} + +func aesGCM(key []byte) (cipher.AEAD, error) { + blk, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return cipher.NewGCM(blk) +} + +// AESGCM creates a new Cipher with a pre-shared key. len(psk) must be +// one of 16, 24, or 32 to select AES-128/196/256-GCM. +func AESGCM(psk []byte) (Cipher, error) { + switch l := len(psk); l { + case 16, 24, 32: // AES 128/196/256 + default: + return nil, aes.KeySizeError(l) + } + return &metaCipher{psk: psk, makeAEAD: aesGCM}, nil +} + +// Chacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk) +// must be 32. +func Chacha20Poly1305(psk []byte) (Cipher, error) { + if len(psk) != chacha20poly1305.KeySize { + return nil, KeySizeError(chacha20poly1305.KeySize) + } + return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.New}, nil +} + +// XChacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk) +// must be 32. +func XChacha20Poly1305(psk []byte) (Cipher, error) { + if len(psk) != chacha20poly1305.KeySize { + return nil, KeySizeError(chacha20poly1305.KeySize) + } + return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.NewX}, nil +} diff --git a/transport/shadowsocks/shadowaead/packet.go b/transport/shadowsocks/shadowaead/packet.go new file mode 100644 index 00000000..7043ead7 --- /dev/null +++ b/transport/shadowsocks/shadowaead/packet.go @@ -0,0 +1,95 @@ +package shadowaead + +import ( + "crypto/rand" + "errors" + "io" + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +// ErrShortPacket means that the packet is too short for a valid encrypted packet. +var ErrShortPacket = errors.New("short packet") + +var _zerononce [128]byte // read-only. 128 bytes is more than enough. + +// Pack encrypts plaintext using Cipher with a randomly generated salt and +// returns a slice of dst containing the encrypted packet and any error occurred. +// Ensure len(dst) >= ciph.SaltSize() + len(plaintext) + aead.Overhead(). +func Pack(dst, plaintext []byte, ciph Cipher) ([]byte, error) { + saltSize := ciph.SaltSize() + salt := dst[:saltSize] + if _, err := rand.Read(salt); err != nil { + return nil, err + } + aead, err := ciph.Encrypter(salt) + if err != nil { + return nil, err + } + if len(dst) < saltSize+len(plaintext)+aead.Overhead() { + return nil, io.ErrShortBuffer + } + b := aead.Seal(dst[saltSize:saltSize], _zerononce[:aead.NonceSize()], plaintext, nil) + return dst[:saltSize+len(b)], nil +} + +// Unpack decrypts pkt using Cipher and returns a slice of dst containing the decrypted payload and any error occurred. +// Ensure len(dst) >= len(pkt) - aead.SaltSize() - aead.Overhead(). +func Unpack(dst, pkt []byte, ciph Cipher) ([]byte, error) { + saltSize := ciph.SaltSize() + if len(pkt) < saltSize { + return nil, ErrShortPacket + } + salt := pkt[:saltSize] + aead, err := ciph.Decrypter(salt) + if err != nil { + return nil, err + } + if len(pkt) < saltSize+aead.Overhead() { + return nil, ErrShortPacket + } + if saltSize+len(dst)+aead.Overhead() < len(pkt) { + return nil, io.ErrShortBuffer + } + b, err := aead.Open(dst[:0], _zerononce[:aead.NonceSize()], pkt[saltSize:], nil) + return b, err +} + +type PacketConn struct { + net.PacketConn + Cipher +} + +const maxPacketSize = 64 * 1024 + +// NewPacketConn wraps a net.PacketConn with cipher +func NewPacketConn(c net.PacketConn, ciph Cipher) *PacketConn { + return &PacketConn{PacketConn: c, Cipher: ciph} +} + +// WriteTo encrypts b and write to addr using the embedded PacketConn. +func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + buf := pool.Get(maxPacketSize) + defer pool.Put(buf) + buf, err := Pack(buf, b, c) + if err != nil { + return 0, err + } + _, err = c.PacketConn.WriteTo(buf, addr) + return len(b), err +} + +// ReadFrom reads from the embedded PacketConn and decrypts into b. +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, err := Unpack(b[c.Cipher.SaltSize():], b[:n], c) + if err != nil { + return n, addr, err + } + copy(b, bb) + return len(bb), addr, err +} diff --git a/transport/shadowsocks/shadowaead/stream.go b/transport/shadowsocks/shadowaead/stream.go new file mode 100644 index 00000000..e92bddab --- /dev/null +++ b/transport/shadowsocks/shadowaead/stream.go @@ -0,0 +1,285 @@ +package shadowaead + +import ( + "crypto/cipher" + "crypto/rand" + "errors" + "io" + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +const ( + // payloadSizeMask is the maximum size of payload in bytes. + payloadSizeMask = 0x3FFF // 16*1024 - 1 + bufSize = 17 * 1024 // >= 2+aead.Overhead()+payloadSizeMask+aead.Overhead() +) + +var ErrZeroChunk = errors.New("zero chunk") + +type Writer struct { + io.Writer + cipher.AEAD + nonce [32]byte // should be sufficient for most nonce sizes +} + +// NewWriter wraps an io.Writer with authenticated encryption. +func NewWriter(w io.Writer, aead cipher.AEAD) *Writer { return &Writer{Writer: w, AEAD: aead} } + +// Write encrypts p and writes to the embedded io.Writer. +func (w *Writer) Write(p []byte) (n int, err error) { + buf := pool.Get(bufSize) + defer pool.Put(buf) + nonce := w.nonce[:w.NonceSize()] + tag := w.Overhead() + off := 2 + tag + + // compatible with snell + if len(p) == 0 { + buf = buf[:off] + buf[0], buf[1] = byte(0), byte(0) + w.Seal(buf[:0], nonce, buf[:2], nil) + increment(nonce) + _, err = w.Writer.Write(buf) + return + } + + for nr := 0; n < len(p) && err == nil; n += nr { + nr = payloadSizeMask + if n+nr > len(p) { + nr = len(p) - n + } + buf = buf[:off+nr+tag] + buf[0], buf[1] = byte(nr>>8), byte(nr) // big-endian payload size + w.Seal(buf[:0], nonce, buf[:2], nil) + increment(nonce) + w.Seal(buf[:off], nonce, p[n:n+nr], nil) + increment(nonce) + _, err = w.Writer.Write(buf) + } + return +} + +// ReadFrom reads from the given io.Reader until EOF or error, encrypts and +// writes to the embedded io.Writer. Returns number of bytes read from r and +// any error encountered. +func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) { + buf := pool.Get(bufSize) + defer pool.Put(buf) + nonce := w.nonce[:w.NonceSize()] + tag := w.Overhead() + off := 2 + tag + for { + nr, er := r.Read(buf[off : off+payloadSizeMask]) + n += int64(nr) + buf[0], buf[1] = byte(nr>>8), byte(nr) + w.Seal(buf[:0], nonce, buf[:2], nil) + increment(nonce) + w.Seal(buf[:off], nonce, buf[off:off+nr], nil) + increment(nonce) + if _, ew := w.Writer.Write(buf[:off+nr+tag]); ew != nil { + err = ew + return + } + if er != nil { + if er != io.EOF { // ignore EOF as per io.ReaderFrom contract + err = er + } + return + } + } +} + +type Reader struct { + io.Reader + cipher.AEAD + nonce [32]byte // should be sufficient for most nonce sizes + buf []byte // to be put back into bufPool + off int // offset to unconsumed part of buf +} + +// NewReader wraps an io.Reader with authenticated decryption. +func NewReader(r io.Reader, aead cipher.AEAD) *Reader { return &Reader{Reader: r, AEAD: aead} } + +// Read and decrypt a record into p. len(p) >= max payload size + AEAD overhead. +func (r *Reader) read(p []byte) (int, error) { + nonce := r.nonce[:r.NonceSize()] + tag := r.Overhead() + + // decrypt payload size + p = p[:2+tag] + if _, err := io.ReadFull(r.Reader, p); err != nil { + return 0, err + } + _, err := r.Open(p[:0], nonce, p, nil) + increment(nonce) + if err != nil { + return 0, err + } + + // decrypt payload + size := (int(p[0])<<8 + int(p[1])) & payloadSizeMask + if size == 0 { + return 0, ErrZeroChunk + } + + p = p[:size+tag] + if _, err := io.ReadFull(r.Reader, p); err != nil { + return 0, err + } + _, err = r.Open(p[:0], nonce, p, nil) + increment(nonce) + if err != nil { + return 0, err + } + return size, nil +} + +// Read reads from the embedded io.Reader, decrypts and writes to p. +func (r *Reader) Read(p []byte) (int, error) { + if r.buf == nil { + if len(p) >= payloadSizeMask+r.Overhead() { + return r.read(p) + } + b := pool.Get(bufSize) + n, err := r.read(b) + if err != nil { + return 0, err + } + r.buf = b[:n] + r.off = 0 + } + + n := copy(p, r.buf[r.off:]) + r.off += n + if r.off == len(r.buf) { + pool.Put(r.buf[:cap(r.buf)]) + r.buf = nil + } + return n, nil +} + +// WriteTo reads from the embedded io.Reader, decrypts and writes to w until +// there's no more data to write or when an error occurs. Return number of +// bytes written to w and any error encountered. +func (r *Reader) WriteTo(w io.Writer) (n int64, err error) { + if r.buf == nil { + r.buf = pool.Get(bufSize) + r.off = len(r.buf) + } + + for { + for r.off < len(r.buf) { + nw, ew := w.Write(r.buf[r.off:]) + r.off += nw + n += int64(nw) + if ew != nil { + if r.off == len(r.buf) { + pool.Put(r.buf[:cap(r.buf)]) + r.buf = nil + } + err = ew + return + } + } + + nr, er := r.read(r.buf) + if er != nil { + if er != io.EOF { + err = er + } + return + } + r.buf = r.buf[:nr] + r.off = 0 + } +} + +// increment little-endian encoded unsigned integer b. Wrap around on overflow. +func increment(b []byte) { + for i := range b { + b[i]++ + if b[i] != 0 { + return + } + } +} + +type Conn struct { + net.Conn + Cipher + r *Reader + w *Writer +} + +// NewConn wraps a stream-oriented net.Conn with cipher. +func NewConn(c net.Conn, ciph Cipher) *Conn { return &Conn{Conn: c, Cipher: ciph} } + +func (c *Conn) initReader() error { + salt := make([]byte, c.SaltSize()) + if _, err := io.ReadFull(c.Conn, salt); err != nil { + return err + } + + aead, err := c.Decrypter(salt) + if err != nil { + return err + } + + c.r = NewReader(c.Conn, aead) + return nil +} + +func (c *Conn) Read(b []byte) (int, error) { + if c.r == nil { + if err := c.initReader(); err != nil { + return 0, err + } + } + return c.r.Read(b) +} + +func (c *Conn) WriteTo(w io.Writer) (int64, error) { + if c.r == nil { + if err := c.initReader(); err != nil { + return 0, err + } + } + return c.r.WriteTo(w) +} + +func (c *Conn) initWriter() error { + salt := make([]byte, c.SaltSize()) + if _, err := rand.Read(salt); err != nil { + return err + } + aead, err := c.Encrypter(salt) + if err != nil { + return err + } + _, err = c.Conn.Write(salt) + if err != nil { + return err + } + c.w = NewWriter(c.Conn, aead) + return nil +} + +func (c *Conn) Write(b []byte) (int, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + return c.w.Write(b) +} + +func (c *Conn) ReadFrom(r io.Reader) (int64, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + return c.w.ReadFrom(r) +} diff --git a/transport/shadowsocks/shadowstream/cipher.go b/transport/shadowsocks/shadowstream/cipher.go new file mode 100644 index 00000000..dd39d03b --- /dev/null +++ b/transport/shadowsocks/shadowstream/cipher.go @@ -0,0 +1,116 @@ +package shadowstream + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/rc4" + "strconv" + + "golang.org/x/crypto/chacha20" +) + +// Cipher generates a pair of stream ciphers for encryption and decryption. +type Cipher interface { + IVSize() int + Encrypter(iv []byte) cipher.Stream + Decrypter(iv []byte) cipher.Stream +} + +type KeySizeError int + +func (e KeySizeError) Error() string { + return "key size error: need " + strconv.Itoa(int(e)) + " bytes" +} + +// CTR mode +type ctrStream struct{ cipher.Block } + +func (b *ctrStream) IVSize() int { return b.BlockSize() } +func (b *ctrStream) Decrypter(iv []byte) cipher.Stream { return b.Encrypter(iv) } +func (b *ctrStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCTR(b, iv) } + +func AESCTR(key []byte) (Cipher, error) { + blk, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return &ctrStream{blk}, nil +} + +// CFB mode +type cfbStream struct{ cipher.Block } + +func (b *cfbStream) IVSize() int { return b.BlockSize() } +func (b *cfbStream) Decrypter(iv []byte) cipher.Stream { return cipher.NewCFBDecrypter(b, iv) } +func (b *cfbStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCFBEncrypter(b, iv) } + +func AESCFB(key []byte) (Cipher, error) { + blk, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return &cfbStream{blk}, nil +} + +// IETF-variant of chacha20 +type chacha20ietfkey []byte + +func (k chacha20ietfkey) IVSize() int { return chacha20.NonceSize } +func (k chacha20ietfkey) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) } +func (k chacha20ietfkey) Encrypter(iv []byte) cipher.Stream { + ciph, err := chacha20.NewUnauthenticatedCipher(k, iv) + if err != nil { + panic(err) // should never happen + } + return ciph +} + +func Chacha20IETF(key []byte) (Cipher, error) { + if len(key) != chacha20.KeySize { + return nil, KeySizeError(chacha20.KeySize) + } + return chacha20ietfkey(key), nil +} + +type xchacha20key []byte + +func (k xchacha20key) IVSize() int { return chacha20.NonceSizeX } +func (k xchacha20key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) } +func (k xchacha20key) Encrypter(iv []byte) cipher.Stream { + ciph, err := chacha20.NewUnauthenticatedCipher(k, iv) + if err != nil { + panic(err) // should never happen + } + return ciph +} + +func Xchacha20(key []byte) (Cipher, error) { + if len(key) != chacha20.KeySize { + return nil, KeySizeError(chacha20.KeySize) + } + return xchacha20key(key), nil +} + +type rc4Md5Key []byte + +func (k rc4Md5Key) IVSize() int { + return 16 +} + +func (k rc4Md5Key) Encrypter(iv []byte) cipher.Stream { + h := md5.New() + h.Write([]byte(k)) + h.Write(iv) + rc4key := h.Sum(nil) + c, _ := rc4.NewCipher(rc4key) + return c +} + +func (k rc4Md5Key) Decrypter(iv []byte) cipher.Stream { + return k.Encrypter(iv) +} + +func RC4MD5(key []byte) (Cipher, error) { + return rc4Md5Key(key), nil +} diff --git a/transport/shadowsocks/shadowstream/packet.go b/transport/shadowsocks/shadowstream/packet.go new file mode 100644 index 00000000..0b46dea1 --- /dev/null +++ b/transport/shadowsocks/shadowstream/packet.go @@ -0,0 +1,79 @@ +package shadowstream + +import ( + "crypto/rand" + "errors" + "io" + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +// ErrShortPacket means the packet is too short to be a valid encrypted packet. +var ErrShortPacket = errors.New("short packet") + +// Pack encrypts plaintext using stream cipher s and a random IV. +// Returns a slice of dst containing random IV and ciphertext. +// Ensure len(dst) >= s.IVSize() + len(plaintext). +func Pack(dst, plaintext []byte, s Cipher) ([]byte, error) { + if len(dst) < s.IVSize()+len(plaintext) { + return nil, io.ErrShortBuffer + } + iv := dst[:s.IVSize()] + _, err := rand.Read(iv) + if err != nil { + return nil, err + } + s.Encrypter(iv).XORKeyStream(dst[len(iv):], plaintext) + return dst[:len(iv)+len(plaintext)], nil +} + +// Unpack decrypts pkt using stream cipher s. +// Returns a slice of dst containing decrypted plaintext. +func Unpack(dst, pkt []byte, s Cipher) ([]byte, error) { + if len(pkt) < s.IVSize() { + return nil, ErrShortPacket + } + if len(dst) < len(pkt)-s.IVSize() { + return nil, io.ErrShortBuffer + } + iv := pkt[:s.IVSize()] + s.Decrypter(iv).XORKeyStream(dst, pkt[len(iv):]) + return dst[:len(pkt)-len(iv)], nil +} + +type PacketConn struct { + net.PacketConn + Cipher +} + +// NewPacketConn wraps a net.PacketConn with stream cipher encryption/decryption. +func NewPacketConn(c net.PacketConn, ciph Cipher) *PacketConn { + return &PacketConn{PacketConn: c, Cipher: ciph} +} + +const maxPacketSize = 64 * 1024 + +func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + buf := pool.Get(maxPacketSize) + defer pool.Put(buf) + buf, err := Pack(buf, b, c.Cipher) + 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, err := Unpack(b[c.IVSize():], b[:n], c.Cipher) + if err != nil { + return n, addr, err + } + copy(b, bb) + return len(bb), addr, err +} diff --git a/transport/shadowsocks/shadowstream/stream.go b/transport/shadowsocks/shadowstream/stream.go new file mode 100644 index 00000000..6c4b0f6d --- /dev/null +++ b/transport/shadowsocks/shadowstream/stream.go @@ -0,0 +1,197 @@ +package shadowstream + +import ( + "crypto/cipher" + "crypto/rand" + "io" + "net" +) + +const bufSize = 2048 + +type Writer struct { + io.Writer + cipher.Stream + buf [bufSize]byte +} + +// NewWriter wraps an io.Writer with stream cipher encryption. +func NewWriter(w io.Writer, s cipher.Stream) *Writer { return &Writer{Writer: w, Stream: s} } + +func (w *Writer) Write(p []byte) (n int, err error) { + buf := w.buf[:] + for nw := 0; n < len(p) && err == nil; n += nw { + end := n + len(buf) + if end > len(p) { + end = len(p) + } + w.XORKeyStream(buf, p[n:end]) + nw, err = w.Writer.Write(buf[:end-n]) + } + return +} + +func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) { + buf := w.buf[:] + for { + nr, er := r.Read(buf) + n += int64(nr) + b := buf[:nr] + w.XORKeyStream(b, b) + if _, err = w.Writer.Write(b); err != nil { + return + } + if er != nil { + if er != io.EOF { // ignore EOF as per io.ReaderFrom contract + err = er + } + return + } + } +} + +type Reader struct { + io.Reader + cipher.Stream + buf [bufSize]byte +} + +// NewReader wraps an io.Reader with stream cipher decryption. +func NewReader(r io.Reader, s cipher.Stream) *Reader { return &Reader{Reader: r, Stream: s} } + +func (r *Reader) Read(p []byte) (n int, err error) { + n, err = r.Reader.Read(p) + if err != nil { + return 0, err + } + r.XORKeyStream(p, p[:n]) + return +} + +func (r *Reader) WriteTo(w io.Writer) (n int64, err error) { + buf := r.buf[:] + for { + nr, er := r.Reader.Read(buf) + if nr > 0 { + r.XORKeyStream(buf, buf[:nr]) + nw, ew := w.Write(buf[:nr]) + n += int64(nw) + if ew != nil { + err = ew + return + } + } + if er != nil { + if er != io.EOF { // ignore EOF as per io.Copy contract (using src.WriteTo shortcut) + err = er + } + return + } + } +} + +// A Conn represents a Shadowsocks connection. It implements the net.Conn interface. +type Conn struct { + net.Conn + Cipher + r *Reader + w *Writer + readIV []byte + writeIV []byte +} + +// NewConn wraps a stream-oriented net.Conn with stream cipher encryption/decryption. +func NewConn(c net.Conn, ciph Cipher) *Conn { return &Conn{Conn: c, Cipher: ciph} } + +func (c *Conn) initReader() error { + if c.r == nil { + iv, err := c.ObtainReadIV() + if err != nil { + return err + } + c.r = NewReader(c.Conn, c.Decrypter(iv)) + } + return nil +} + +func (c *Conn) Read(b []byte) (int, error) { + if c.r == nil { + if err := c.initReader(); err != nil { + return 0, err + } + } + return c.r.Read(b) +} + +func (c *Conn) WriteTo(w io.Writer) (int64, error) { + if c.r == nil { + if err := c.initReader(); err != nil { + return 0, err + } + } + return c.r.WriteTo(w) +} + +func (c *Conn) initWriter() error { + if c.w == nil { + iv, err := c.ObtainWriteIV() + if err != nil { + return err + } + if _, err := c.Conn.Write(iv); err != nil { + return err + } + c.w = NewWriter(c.Conn, c.Encrypter(iv)) + } + return nil +} + +func (c *Conn) Write(b []byte) (int, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + return c.w.Write(b) +} + +func (c *Conn) ReadFrom(r io.Reader) (int64, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + return c.w.ReadFrom(r) +} + +func (c *Conn) ObtainWriteIV() ([]byte, error) { + if len(c.writeIV) == c.IVSize() { + return c.writeIV, nil + } + + iv := make([]byte, c.IVSize()) + + if _, err := rand.Read(iv); err != nil { + return nil, err + } + + c.writeIV = iv + + return iv, nil +} + +func (c *Conn) ObtainReadIV() ([]byte, error) { + if len(c.readIV) == c.IVSize() { + return c.readIV, nil + } + + iv := make([]byte, c.IVSize()) + + if _, err := io.ReadFull(c.Conn, iv); err != nil { + return nil, err + } + + c.readIV = iv + + return iv, nil +} diff --git a/transport/snell/cipher.go b/transport/snell/cipher.go index 0f31aea5..24999e28 100644 --- a/transport/snell/cipher.go +++ b/transport/snell/cipher.go @@ -4,7 +4,8 @@ import ( "crypto/aes" "crypto/cipher" - "github.com/Dreamacro/go-shadowsocks2/shadowaead" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" + "golang.org/x/crypto/argon2" "golang.org/x/crypto/chacha20poly1305" ) diff --git a/transport/snell/pool.go b/transport/snell/pool.go index 237baf21..2a233a75 100644 --- a/transport/snell/pool.go +++ b/transport/snell/pool.go @@ -6,8 +6,7 @@ import ( "time" "github.com/Dreamacro/clash/component/pool" - - "github.com/Dreamacro/go-shadowsocks2/shadowaead" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" ) type Pool struct { diff --git a/transport/snell/snell.go b/transport/snell/snell.go index 4cd5fba8..c6b7a569 100644 --- a/transport/snell/snell.go +++ b/transport/snell/snell.go @@ -9,9 +9,8 @@ import ( "sync" "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" "github.com/Dreamacro/clash/transport/socks5" - - "github.com/Dreamacro/go-shadowsocks2/shadowaead" ) const ( diff --git a/transport/ssr/protocol/auth_chain_a.go b/transport/ssr/protocol/auth_chain_a.go index 906f8deb..6b12ab9b 100644 --- a/transport/ssr/protocol/auth_chain_a.go +++ b/transport/ssr/protocol/auth_chain_a.go @@ -13,9 +13,8 @@ import ( "github.com/Dreamacro/clash/common/pool" "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/transport/shadowsocks/core" "github.com/Dreamacro/clash/transport/ssr/tools" - - "github.com/Dreamacro/go-shadowsocks2/core" ) func init() { diff --git a/transport/ssr/protocol/base.go b/transport/ssr/protocol/base.go index 1ed348d0..4bf799b3 100644 --- a/transport/ssr/protocol/base.go +++ b/transport/ssr/protocol/base.go @@ -12,8 +12,7 @@ import ( "github.com/Dreamacro/clash/common/pool" "github.com/Dreamacro/clash/log" - - "github.com/Dreamacro/go-shadowsocks2/core" + "github.com/Dreamacro/clash/transport/shadowsocks/core" ) type Base struct { diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index 6a7d0723..8e6a86a9 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -192,13 +192,13 @@ func preHandleMetadata(metadata *C.Metadata) error { } // pre resolve process name - srcPort, err := strconv.Atoi(metadata.SrcPort) + srcPort, err := strconv.ParseUint(metadata.SrcPort, 10, 16) if err == nil && P.ShouldFindProcess(metadata) { - path, err := P.FindProcessName(metadata.NetWork.String(), metadata.SrcIP, srcPort) + path, err := P.FindProcessName(metadata.NetWork.String(), metadata.SrcIP, int(srcPort)) if err != nil { log.Debugln("[Process] find process %s: %v", metadata.String(), err) } else { - // log.Debugln("[Process] %s from process %s", metadata.String(), path) + log.Debugln("[Process] %s from process %s", metadata.String(), path) metadata.Process = filepath.Base(path) metadata.ProcessPath = path }