From a4d135ed215816e7ca777b6aac0bc55a514264c9 Mon Sep 17 00:00:00 2001 From: yaling888 <73897884+yaling888@users.noreply.github.com> Date: Fri, 3 Jun 2022 05:05:42 +0800 Subject: [PATCH] Feature: add regexp filter to use proxy provider in proxy group --- README.md | 35 ++++++++++- adapter/outboundgroup/parser.go | 49 +++++++++++++-- adapter/provider/healthcheck.go | 7 ++- adapter/provider/provider.go | 106 ++++++++++++++++++++++++++++++-- config/config.go | 2 +- hub/route/proxies.go | 22 +++++++ 6 files changed, 210 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 690534e2..84cdc612 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,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 @@ -272,6 +274,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/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/provider.go b/adapter/provider/provider.go index 5e6e6fab..571a60ff 100644 --- a/adapter/provider/provider.go +++ b/adapter/provider/provider.go @@ -31,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) { @@ -87,11 +88,16 @@ 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() @@ -197,6 +203,98 @@ 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) diff --git a/config/config.go b/config/config.go index 2e197d34..9d900452 100644 --- a/config/config.go +++ b/config/config.go @@ -441,7 +441,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/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 +}